diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /accessible/generic | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
31 files changed, 15651 insertions, 0 deletions
diff --git a/accessible/generic/ARIAGridAccessible-inl.h b/accessible/generic/ARIAGridAccessible-inl.h new file mode 100644 index 0000000000..ed2c6e2278 --- /dev/null +++ b/accessible/generic/ARIAGridAccessible-inl.h @@ -0,0 +1,36 @@ +/* -*- 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_ARIAGridAccessible_inl_h__ +#define mozilla_a11y_ARIAGridAccessible_inl_h__ + +#include "ARIAGridAccessible.h" + +#include "AccIterator.h" +#include "nsAccUtils.h" + +namespace mozilla { +namespace a11y { + +inline int32_t ARIAGridCellAccessible::RowIndexFor( + LocalAccessible* aRow) const { + LocalAccessible* table = nsAccUtils::TableFor(aRow); + if (table) { + int32_t rowIdx = 0; + LocalAccessible* row = nullptr; + AccIterator rowIter(table, filters::GetRow); + while ((row = rowIter.Next()) && row != aRow) rowIdx++; + + if (row) return rowIdx; + } + + return -1; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/ARIAGridAccessible.cpp b/accessible/generic/ARIAGridAccessible.cpp new file mode 100644 index 0000000000..399370835a --- /dev/null +++ b/accessible/generic/ARIAGridAccessible.cpp @@ -0,0 +1,625 @@ +/* -*- 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 "ARIAGridAccessible-inl.h" + +#include "LocalAccessible-inl.h" +#include "AccAttributes.h" +#include "AccIterator.h" +#include "nsAccUtils.h" +#include "Role.h" +#include "States.h" + +#include "mozilla/dom/Element.h" +#include "nsComponentManagerUtils.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// ARIAGridAccessible +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Constructor + +ARIAGridAccessible::ARIAGridAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : HyperTextAccessibleWrap(aContent, aDoc) { + mGenericTypes |= eTable; +} + +role ARIAGridAccessible::NativeRole() const { + a11y::role r = GetAccService()->MarkupRole(mContent); + return r != roles::NOTHING ? r : roles::TABLE; +} + +already_AddRefed<AccAttributes> ARIAGridAccessible::NativeAttributes() { + RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes(); + + if (IsProbablyLayoutTable()) { + attributes->SetAttribute(nsGkAtoms::layout_guess, true); + } + + return attributes.forget(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Table + +uint32_t ARIAGridAccessible::ColCount() const { + AccIterator rowIter(this, filters::GetRow); + LocalAccessible* row = rowIter.Next(); + if (!row) return 0; + + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = nullptr; + + uint32_t colCount = 0; + while ((cell = cellIter.Next())) { + MOZ_ASSERT(cell->IsTableCell(), "No table or grid cell!"); + colCount += cell->AsTableCell()->ColExtent(); + } + + return colCount; +} + +uint32_t ARIAGridAccessible::RowCount() { + uint32_t rowCount = 0; + AccIterator rowIter(this, filters::GetRow); + while (rowIter.Next()) rowCount++; + + return rowCount; +} + +LocalAccessible* ARIAGridAccessible::CellAt(uint32_t aRowIndex, + uint32_t aColumnIndex) { + LocalAccessible* row = RowAt(aRowIndex); + if (!row) return nullptr; + + return CellInRowAt(row, aColumnIndex); +} + +bool ARIAGridAccessible::IsColSelected(uint32_t aColIdx) { + if (IsARIARole(nsGkAtoms::table)) return false; + + AccIterator rowIter(this, filters::GetRow); + LocalAccessible* row = rowIter.Next(); + if (!row) return false; + + do { + if (!nsAccUtils::IsARIASelected(row)) { + LocalAccessible* cell = CellInRowAt(row, aColIdx); + if (!cell || !nsAccUtils::IsARIASelected(cell)) return false; + } + } while ((row = rowIter.Next())); + + return true; +} + +bool ARIAGridAccessible::IsRowSelected(uint32_t aRowIdx) { + if (IsARIARole(nsGkAtoms::table)) return false; + + LocalAccessible* row = RowAt(aRowIdx); + if (!row) return false; + + if (!nsAccUtils::IsARIASelected(row)) { + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = nullptr; + while ((cell = cellIter.Next())) { + if (!nsAccUtils::IsARIASelected(cell)) return false; + } + } + + return true; +} + +bool ARIAGridAccessible::IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) { + if (IsARIARole(nsGkAtoms::table)) return false; + + LocalAccessible* row = RowAt(aRowIdx); + if (!row) return false; + + if (!nsAccUtils::IsARIASelected(row)) { + LocalAccessible* cell = CellInRowAt(row, aColIdx); + if (!cell || !nsAccUtils::IsARIASelected(cell)) return false; + } + + return true; +} + +uint32_t ARIAGridAccessible::SelectedCellCount() { + if (IsARIARole(nsGkAtoms::table)) return 0; + + uint32_t count = 0, colCount = ColCount(); + + AccIterator rowIter(this, filters::GetRow); + LocalAccessible* row = nullptr; + + while ((row = rowIter.Next())) { + if (nsAccUtils::IsARIASelected(row)) { + count += colCount; + continue; + } + + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = nullptr; + + while ((cell = cellIter.Next())) { + if (nsAccUtils::IsARIASelected(cell)) count++; + } + } + + return count; +} + +uint32_t ARIAGridAccessible::SelectedColCount() { + if (IsARIARole(nsGkAtoms::table)) return 0; + + uint32_t colCount = ColCount(); + if (!colCount) return 0; + + AccIterator rowIter(this, filters::GetRow); + LocalAccessible* row = rowIter.Next(); + if (!row) return 0; + + nsTArray<bool> isColSelArray(colCount); + isColSelArray.AppendElements(colCount); + memset(isColSelArray.Elements(), true, colCount * sizeof(bool)); + + uint32_t selColCount = colCount; + do { + if (nsAccUtils::IsARIASelected(row)) continue; + + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = nullptr; + for (uint32_t colIdx = 0; (cell = cellIter.Next()) && colIdx < colCount; + colIdx++) { + if (isColSelArray[colIdx] && !nsAccUtils::IsARIASelected(cell)) { + isColSelArray[colIdx] = false; + selColCount--; + } + } + } while ((row = rowIter.Next())); + + return selColCount; +} + +uint32_t ARIAGridAccessible::SelectedRowCount() { + if (IsARIARole(nsGkAtoms::table)) return 0; + + uint32_t count = 0; + + AccIterator rowIter(this, filters::GetRow); + LocalAccessible* row = nullptr; + + while ((row = rowIter.Next())) { + if (nsAccUtils::IsARIASelected(row)) { + count++; + continue; + } + + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = cellIter.Next(); + if (!cell) continue; + + bool isRowSelected = true; + do { + if (!nsAccUtils::IsARIASelected(cell)) { + isRowSelected = false; + break; + } + } while ((cell = cellIter.Next())); + + if (isRowSelected) count++; + } + + return count; +} + +void ARIAGridAccessible::SelectedCells(nsTArray<Accessible*>* aCells) { + if (IsARIARole(nsGkAtoms::table)) return; + + AccIterator rowIter(this, filters::GetRow); + + LocalAccessible* row = nullptr; + while ((row = rowIter.Next())) { + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = nullptr; + + if (nsAccUtils::IsARIASelected(row)) { + while ((cell = cellIter.Next())) aCells->AppendElement(cell); + + continue; + } + + while ((cell = cellIter.Next())) { + if (nsAccUtils::IsARIASelected(cell)) aCells->AppendElement(cell); + } + } +} + +void ARIAGridAccessible::SelectedCellIndices(nsTArray<uint32_t>* aCells) { + if (IsARIARole(nsGkAtoms::table)) return; + + uint32_t colCount = ColCount(); + + AccIterator rowIter(this, filters::GetRow); + LocalAccessible* row = nullptr; + for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) { + if (nsAccUtils::IsARIASelected(row)) { + for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) { + aCells->AppendElement(rowIdx * colCount + colIdx); + } + + continue; + } + + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = nullptr; + for (uint32_t colIdx = 0; (cell = cellIter.Next()); colIdx++) { + if (nsAccUtils::IsARIASelected(cell)) { + aCells->AppendElement(rowIdx * colCount + colIdx); + } + } + } +} + +void ARIAGridAccessible::SelectedColIndices(nsTArray<uint32_t>* aCols) { + if (IsARIARole(nsGkAtoms::table)) return; + + uint32_t colCount = ColCount(); + if (!colCount) return; + + AccIterator rowIter(this, filters::GetRow); + LocalAccessible* row = rowIter.Next(); + if (!row) return; + + nsTArray<bool> isColSelArray(colCount); + isColSelArray.AppendElements(colCount); + memset(isColSelArray.Elements(), true, colCount * sizeof(bool)); + + do { + if (nsAccUtils::IsARIASelected(row)) continue; + + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = nullptr; + for (uint32_t colIdx = 0; (cell = cellIter.Next()) && colIdx < colCount; + colIdx++) { + if (isColSelArray[colIdx] && !nsAccUtils::IsARIASelected(cell)) { + isColSelArray[colIdx] = false; + } + } + } while ((row = rowIter.Next())); + + for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) { + if (isColSelArray[colIdx]) aCols->AppendElement(colIdx); + } +} + +void ARIAGridAccessible::SelectedRowIndices(nsTArray<uint32_t>* aRows) { + if (IsARIARole(nsGkAtoms::table)) return; + + AccIterator rowIter(this, filters::GetRow); + LocalAccessible* row = nullptr; + for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) { + if (nsAccUtils::IsARIASelected(row)) { + aRows->AppendElement(rowIdx); + continue; + } + + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = cellIter.Next(); + if (!cell) continue; + + bool isRowSelected = true; + do { + if (!nsAccUtils::IsARIASelected(cell)) { + isRowSelected = false; + break; + } + } while ((cell = cellIter.Next())); + + if (isRowSelected) aRows->AppendElement(rowIdx); + } +} + +void ARIAGridAccessible::SelectRow(uint32_t aRowIdx) { + if (IsARIARole(nsGkAtoms::table)) return; + + AccIterator rowIter(this, filters::GetRow); + + LocalAccessible* row = nullptr; + for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) { + DebugOnly<nsresult> rv = SetARIASelected(row, rowIdx == aRowIdx); + NS_ASSERTION(NS_SUCCEEDED(rv), "SetARIASelected() Shouldn't fail!"); + } +} + +void ARIAGridAccessible::SelectCol(uint32_t aColIdx) { + if (IsARIARole(nsGkAtoms::table)) return; + + AccIterator rowIter(this, filters::GetRow); + + LocalAccessible* row = nullptr; + while ((row = rowIter.Next())) { + // Unselect all cells in the row. + DebugOnly<nsresult> rv = SetARIASelected(row, false); + NS_ASSERTION(NS_SUCCEEDED(rv), "SetARIASelected() Shouldn't fail!"); + + // Select cell at the column index. + LocalAccessible* cell = CellInRowAt(row, aColIdx); + if (cell) SetARIASelected(cell, true); + } +} + +void ARIAGridAccessible::UnselectRow(uint32_t aRowIdx) { + if (IsARIARole(nsGkAtoms::table)) return; + + LocalAccessible* row = RowAt(aRowIdx); + if (row) SetARIASelected(row, false); +} + +void ARIAGridAccessible::UnselectCol(uint32_t aColIdx) { + if (IsARIARole(nsGkAtoms::table)) return; + + AccIterator rowIter(this, filters::GetRow); + + LocalAccessible* row = nullptr; + while ((row = rowIter.Next())) { + LocalAccessible* cell = CellInRowAt(row, aColIdx); + if (cell) SetARIASelected(cell, false); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Protected + +nsresult ARIAGridAccessible::SetARIASelected(LocalAccessible* aAccessible, + bool aIsSelected, bool aNotify) { + if (IsARIARole(nsGkAtoms::table)) return NS_OK; + + nsIContent* content = aAccessible->GetContent(); + NS_ENSURE_STATE(content); + + nsresult rv = NS_OK; + if (content->IsElement()) { + if (aIsSelected) { + rv = content->AsElement()->SetAttr( + kNameSpaceID_None, nsGkAtoms::aria_selected, u"true"_ns, aNotify); + } else { + rv = content->AsElement()->SetAttr( + kNameSpaceID_None, nsGkAtoms::aria_selected, u"false"_ns, aNotify); + } + } + + NS_ENSURE_SUCCESS(rv, rv); + + // No "smart" select/unselect for internal call. + if (!aNotify) return NS_OK; + + // If row or cell accessible was selected then we're able to not bother about + // selection of its cells or its row because our algorithm is row oriented, + // i.e. we check selection on row firstly and then on cells. + if (aIsSelected) return NS_OK; + + roles::Role role = aAccessible->Role(); + + // If the given accessible is row that was unselected then remove + // aria-selected from cell accessible. + if (role == roles::ROW) { + AccIterator cellIter(aAccessible, filters::GetCell); + LocalAccessible* cell = nullptr; + + while ((cell = cellIter.Next())) { + rv = SetARIASelected(cell, false, false); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; + } + + // If the given accessible is cell that was unselected and its row is selected + // then remove aria-selected from row and put aria-selected on + // siblings cells. + if (role == roles::GRID_CELL || role == roles::ROWHEADER || + role == roles::COLUMNHEADER) { + LocalAccessible* row = aAccessible->LocalParent(); + + if (row && row->Role() == roles::ROW && nsAccUtils::IsARIASelected(row)) { + rv = SetARIASelected(row, false, false); + NS_ENSURE_SUCCESS(rv, rv); + + AccIterator cellIter(row, filters::GetCell); + LocalAccessible* cell = nullptr; + while ((cell = cellIter.Next())) { + if (cell != aAccessible) { + rv = SetARIASelected(cell, true, false); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// ARIARowAccessible +//////////////////////////////////////////////////////////////////////////////// + +ARIARowAccessible::ARIARowAccessible(nsIContent* aContent, DocAccessible* aDoc) + : HyperTextAccessibleWrap(aContent, aDoc) { + mGenericTypes |= eTableRow; +} + +role ARIARowAccessible::NativeRole() const { + a11y::role r = GetAccService()->MarkupRole(mContent); + return r != roles::NOTHING ? r : roles::ROW; +} + +GroupPos ARIARowAccessible::GroupPosition() { + int32_t count = 0, index = 0; + LocalAccessible* table = nsAccUtils::TableFor(this); + if (table) { + if (nsCoreUtils::GetUIntAttr(table->GetContent(), nsGkAtoms::aria_rowcount, + &count) && + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_rowindex, &index)) { + return GroupPos(0, index, count); + } + + // Deal with the special case here that tables and grids can have rows + // which are wrapped in generic text container elements. Exclude tree grids + // because these are dealt with elsewhere. + if (table->Role() == roles::TABLE) { + LocalAccessible* row = nullptr; + AccIterator rowIter(table, filters::GetRow); + while ((row = rowIter.Next())) { + index++; + if (row == this) { + break; + } + } + + if (row) { + count = table->AsTable()->RowCount(); + return GroupPos(0, index, count); + } + } + } + + return AccessibleWrap::GroupPosition(); +} + +// LocalAccessible protected +ENameValueFlag ARIARowAccessible::NativeName(nsString& aName) const { + // We want to calculate the name from content only if an ARIA role is + // present. ARIARowAccessible might also be used by tables with + // display:block; styling, in which case we do not want the name from + // content. + if (HasStrongARIARole()) { + return AccessibleWrap::NativeName(aName); + } + + return eNameOK; +} + +//////////////////////////////////////////////////////////////////////////////// +// ARIAGridCellAccessible +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Constructor + +ARIAGridCellAccessible::ARIAGridCellAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : HyperTextAccessibleWrap(aContent, aDoc) { + mGenericTypes |= eTableCell; +} + +role ARIAGridCellAccessible::NativeRole() const { + const a11y::role r = GetAccService()->MarkupRole(mContent); + if (r != role::NOTHING) { + return r; + } + + // Special case to handle th elements mapped to ARIA grid cells. + if (GetContent() && GetContent()->IsHTMLElement(nsGkAtoms::th)) { + return GetHeaderCellRole(this); + } + + return role::CELL; +} + +//////////////////////////////////////////////////////////////////////////////// +// TableCell + +TableAccessible* ARIAGridCellAccessible::Table() const { + LocalAccessible* table = nsAccUtils::TableFor(Row()); + return table ? table->AsTable() : nullptr; +} + +uint32_t ARIAGridCellAccessible::ColIdx() const { + LocalAccessible* row = Row(); + if (!row) return 0; + + int32_t indexInRow = IndexInParent(); + uint32_t colIdx = 0; + for (int32_t idx = 0; idx < indexInRow; idx++) { + LocalAccessible* cell = row->LocalChildAt(idx); + if (cell->IsTableCell()) { + colIdx += cell->AsTableCell()->ColExtent(); + } + } + + return colIdx; +} + +uint32_t ARIAGridCellAccessible::RowIdx() const { return RowIndexFor(Row()); } + +bool ARIAGridCellAccessible::Selected() { + LocalAccessible* row = Row(); + if (!row) return false; + + return nsAccUtils::IsARIASelected(row) || nsAccUtils::IsARIASelected(this); +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +void ARIAGridCellAccessible::ApplyARIAState(uint64_t* aState) const { + HyperTextAccessibleWrap::ApplyARIAState(aState); + + // Return if the gridcell has aria-selected="true". + if (*aState & states::SELECTED) return; + + // Check aria-selected="true" on the row. + LocalAccessible* row = LocalParent(); + if (!row || row->Role() != roles::ROW) return; + + nsIContent* rowContent = row->GetContent(); + if (nsAccUtils::HasDefinedARIAToken(rowContent, nsGkAtoms::aria_selected) && + !nsAccUtils::ARIAAttrValueIs(rowContent->AsElement(), + nsGkAtoms::aria_selected, nsGkAtoms::_false, + eCaseMatters)) { + *aState |= states::SELECTABLE | states::SELECTED; + } +} + +already_AddRefed<AccAttributes> ARIAGridCellAccessible::NativeAttributes() { + RefPtr<AccAttributes> attributes = + HyperTextAccessibleWrap::NativeAttributes(); + + // Expose "table-cell-index" attribute. + LocalAccessible* thisRow = Row(); + if (!thisRow) return attributes.forget(); + + int32_t rowIdx = RowIndexFor(thisRow); + if (rowIdx == -1) { // error + return attributes.forget(); + } + + int32_t colIdx = 0, colCount = 0; + uint32_t childCount = thisRow->ChildCount(); + for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { + LocalAccessible* child = thisRow->LocalChildAt(childIdx); + if (child == this) colIdx = colCount; + + roles::Role role = child->Role(); + if (role == roles::CELL || role == roles::GRID_CELL || + role == roles::ROWHEADER || role == roles::COLUMNHEADER) { + colCount++; + } + } + + attributes->SetAttribute(nsGkAtoms::tableCellIndex, + rowIdx * colCount + colIdx); + +#ifdef DEBUG + RefPtr<nsAtom> cppClass = NS_Atomize(u"cppclass"_ns); + attributes->SetAttributeStringCopy(cppClass, u"ARIAGridCellAccessible"_ns); +#endif + + return attributes.forget(); +} diff --git a/accessible/generic/ARIAGridAccessible.h b/accessible/generic/ARIAGridAccessible.h new file mode 100644 index 0000000000..79d48a58d2 --- /dev/null +++ b/accessible/generic/ARIAGridAccessible.h @@ -0,0 +1,133 @@ +/* -*- 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_ARIAGridAccessible_h_ +#define MOZILLA_A11Y_ARIAGridAccessible_h_ + +#include "HyperTextAccessibleWrap.h" +#include "TableAccessible.h" +#include "TableCellAccessible.h" + +namespace mozilla { +namespace a11y { + +/** + * Accessible for ARIA grid and treegrid. + */ +class ARIAGridAccessible : public HyperTextAccessibleWrap, + public TableAccessible { + public: + ARIAGridAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ARIAGridAccessible, + HyperTextAccessibleWrap) + + // LocalAccessible + virtual a11y::role NativeRole() const override; + virtual already_AddRefed<AccAttributes> NativeAttributes() override; + virtual TableAccessible* AsTable() override { return this; } + + // TableAccessible + virtual uint32_t ColCount() const override; + virtual uint32_t RowCount() override; + virtual LocalAccessible* CellAt(uint32_t aRowIndex, + uint32_t aColumnIndex) override; + virtual bool IsColSelected(uint32_t aColIdx) override; + virtual bool IsRowSelected(uint32_t aRowIdx) override; + virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) override; + virtual uint32_t SelectedCellCount() override; + virtual uint32_t SelectedColCount() override; + virtual uint32_t SelectedRowCount() override; + virtual void SelectedCells(nsTArray<Accessible*>* aCells) override; + virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) override; + virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) override; + virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) override; + virtual void SelectCol(uint32_t aColIdx) override; + virtual void SelectRow(uint32_t aRowIdx) override; + virtual void UnselectCol(uint32_t aColIdx) override; + virtual void UnselectRow(uint32_t aRowIdx) override; + virtual LocalAccessible* AsAccessible() override { return this; } + + protected: + virtual ~ARIAGridAccessible() {} + + /** + * Set aria-selected attribute value on DOM node of the given accessible. + * + * @param aAccessible [in] accessible + * @param aIsSelected [in] new value of aria-selected attribute + * @param aNotify [in, optional] specifies if DOM should be notified + * about attribute change (used internally). + */ + nsresult SetARIASelected(LocalAccessible* aAccessible, bool aIsSelected, + bool aNotify = true); +}; + +/** + * Accessible for ARIA row. + */ +class ARIARowAccessible : public HyperTextAccessibleWrap { + public: + ARIARowAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ARIARowAccessible, + HyperTextAccessibleWrap) + + // LocalAccessible + virtual a11y::role NativeRole() const override; + virtual mozilla::a11y::GroupPos GroupPosition() override; + + protected: + virtual ~ARIARowAccessible() {} + + // LocalAccessible + virtual ENameValueFlag NativeName(nsString& aName) const override; +}; + +/** + * Accessible for ARIA gridcell and rowheader/columnheader. + */ +class ARIAGridCellAccessible : public HyperTextAccessibleWrap, + public TableCellAccessible { + public: + ARIAGridCellAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ARIAGridCellAccessible, + HyperTextAccessibleWrap) + + // LocalAccessible + virtual a11y::role NativeRole() const override; + virtual TableCellAccessible* AsTableCell() override { return this; } + virtual void ApplyARIAState(uint64_t* aState) const override; + virtual already_AddRefed<AccAttributes> NativeAttributes() override; + + protected: + virtual ~ARIAGridCellAccessible() {} + + /** + * Return a containing row. + */ + LocalAccessible* Row() const { + LocalAccessible* row = LocalParent(); + return row && row->IsTableRow() ? row : nullptr; + } + + /** + * Return index of the given row. + * Returns -1 upon error. + */ + int32_t RowIndexFor(LocalAccessible* aRow) const; + + // TableCellAccessible + virtual TableAccessible* Table() const override; + virtual uint32_t ColIdx() const override; + virtual uint32_t RowIdx() const override; + virtual bool Selected() override; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/ApplicationAccessible.cpp b/accessible/generic/ApplicationAccessible.cpp new file mode 100644 index 0000000000..8e3baa29e5 --- /dev/null +++ b/accessible/generic/ApplicationAccessible.cpp @@ -0,0 +1,144 @@ +/* -*- 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 "ApplicationAccessible.h" + +#include "AccAttributes.h" +#include "LocalAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "Relation.h" +#include "Role.h" +#include "States.h" + +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/Components.h" +#include "nsGlobalWindow.h" +#include "nsIStringBundle.h" + +using namespace mozilla::a11y; + +ApplicationAccessible::ApplicationAccessible() + : AccessibleWrap(nullptr, nullptr) { + mType = eApplicationType; + mAppInfo = do_GetService("@mozilla.org/xre/app-info;1"); + MOZ_ASSERT(mAppInfo, "no application info"); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessible + +ENameValueFlag ApplicationAccessible::Name(nsString& aName) const { + aName.Truncate(); + + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + + NS_ASSERTION(bundleService, "String bundle service must be present!"); + if (!bundleService) return eNameOK; + + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = bundleService->CreateBundle( + "chrome://branding/locale/brand.properties", getter_AddRefs(bundle)); + if (NS_FAILED(rv)) return eNameOK; + + nsAutoString appName; + rv = bundle->GetStringFromName("brandShortName", appName); + if (NS_FAILED(rv) || appName.IsEmpty()) { + NS_WARNING("brandShortName not found, using default app name"); + appName.AssignLiteral("Gecko based application"); + } + + aName.Assign(appName); + return eNameOK; +} + +void ApplicationAccessible::Description(nsString& aDescription) const { + aDescription.Truncate(); +} + +void ApplicationAccessible::Value(nsString& aValue) const { aValue.Truncate(); } + +uint64_t ApplicationAccessible::State() { + return IsDefunct() ? states::DEFUNCT : 0; +} + +already_AddRefed<AccAttributes> ApplicationAccessible::NativeAttributes() { + RefPtr<AccAttributes> attributes = new AccAttributes(); + return attributes.forget(); +} + +GroupPos ApplicationAccessible::GroupPosition() { return GroupPos(); } + +LocalAccessible* ApplicationAccessible::LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) { + return nullptr; +} + +Accessible* ApplicationAccessible::FocusedChild() { + LocalAccessible* focus = FocusMgr()->FocusedLocalAccessible(); + if (focus && focus->LocalParent() == this) { + return focus; + } + + return nullptr; +} + +Relation ApplicationAccessible::RelationByType( + RelationType aRelationType) const { + return Relation(); +} + +mozilla::LayoutDeviceIntRect ApplicationAccessible::Bounds() const { + return mozilla::LayoutDeviceIntRect(); +} + +nsRect ApplicationAccessible::BoundsInAppUnits() const { return nsRect(); } + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible public methods + +void ApplicationAccessible::Shutdown() { mAppInfo = nullptr; } + +void ApplicationAccessible::ApplyARIAState(uint64_t* aState) const {} + +role ApplicationAccessible::NativeRole() const { return roles::APP_ROOT; } + +uint64_t ApplicationAccessible::NativeState() const { return 0; } + +KeyBinding ApplicationAccessible::AccessKey() const { return KeyBinding(); } + +void ApplicationAccessible::Init() { + // Basically children are kept updated by Append/RemoveChild method calls. + // However if there are open windows before accessibility was started + // then we need to make sure root accessibles for open windows are created so + // that all root accessibles are stored in application accessible children + // array. + + nsGlobalWindowOuter::OuterWindowByIdTable* windowsById = + nsGlobalWindowOuter::GetWindowsTable(); + + if (!windowsById) { + return; + } + + for (const auto& window : windowsById->Values()) { + if (window->GetDocShell() && window->IsRootOuterWindow()) { + if (RefPtr<dom::Document> docNode = window->GetExtantDoc()) { + GetAccService()->GetDocAccessible(docNode); // ensure creation + } + } + } +} + +LocalAccessible* ApplicationAccessible::GetSiblingAtOffset( + int32_t aOffset, nsresult* aError) const { + if (aError) *aError = NS_OK; // fail peacefully + + return nullptr; +} diff --git a/accessible/generic/ApplicationAccessible.h b/accessible/generic/ApplicationAccessible.h new file mode 100644 index 0000000000..1b21ca8e8b --- /dev/null +++ b/accessible/generic/ApplicationAccessible.h @@ -0,0 +1,109 @@ +/* -*- 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_ApplicationAccessible_h__ +#define mozilla_a11y_ApplicationAccessible_h__ + +#include "AccessibleWrap.h" + +#include "nsIXULAppInfo.h" + +namespace mozilla { +namespace a11y { + +/** + * ApplicationAccessible is for the whole application of Mozilla. + * Only one instance of ApplicationAccessible exists for one Mozilla instance. + * And this one should be created when Mozilla Startup (if accessibility + * feature has been enabled) and destroyed when Mozilla Shutdown. + * + * All the accessibility objects for toplevel windows are direct children of + * the ApplicationAccessible instance. + */ + +class ApplicationAccessible : public AccessibleWrap { + public: + ApplicationAccessible(); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ApplicationAccessible, AccessibleWrap) + + // LocalAccessible + virtual void Shutdown() override; + virtual LayoutDeviceIntRect Bounds() const override; + virtual nsRect BoundsInAppUnits() const override; + virtual already_AddRefed<AccAttributes> NativeAttributes() override; + virtual GroupPos GroupPosition() override; + virtual ENameValueFlag Name(nsString& aName) const override; + virtual void ApplyARIAState(uint64_t* aState) const override; + virtual void Description(nsString& aDescription) const override; + virtual void Value(nsString& aValue) const override; + virtual mozilla::a11y::role NativeRole() const override; + virtual uint64_t State() override; + virtual uint64_t NativeState() const override; + virtual Relation RelationByType(RelationType aType) const override; + + virtual LocalAccessible* LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override; + virtual Accessible* FocusedChild() override; + + // ActionAccessible + virtual KeyBinding AccessKey() const override; + + // ApplicationAccessible + void Init(); + + void AppName(nsAString& aName) const { + MOZ_ASSERT(mAppInfo, "no application info"); + + if (mAppInfo) { + nsAutoCString cname; + mAppInfo->GetName(cname); + AppendUTF8toUTF16(cname, aName); + } + } + + void AppVersion(nsAString& aVersion) const { + MOZ_ASSERT(mAppInfo, "no application info"); + + if (mAppInfo) { + nsAutoCString cversion; + mAppInfo->GetVersion(cversion); + AppendUTF8toUTF16(cversion, aVersion); + } + } + + void PlatformName(nsAString& aName) const { aName.AssignLiteral("Gecko"); } + + void PlatformVersion(nsAString& aVersion) const { + MOZ_ASSERT(mAppInfo, "no application info"); + + if (mAppInfo) { + nsAutoCString cversion; + mAppInfo->GetPlatformVersion(cversion); + AppendUTF8toUTF16(cversion, aVersion); + } + } + + protected: + virtual ~ApplicationAccessible() {} + + // LocalAccessible + virtual LocalAccessible* GetSiblingAtOffset( + int32_t aOffset, nsresult* aError = nullptr) const override; + + private: + nsCOMPtr<nsIXULAppInfo> mAppInfo; +}; + +inline ApplicationAccessible* LocalAccessible::AsApplication() { + return IsApplication() ? static_cast<ApplicationAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/BaseAccessibles.cpp b/accessible/generic/BaseAccessibles.cpp new file mode 100644 index 0000000000..085c9a80e4 --- /dev/null +++ b/accessible/generic/BaseAccessibles.cpp @@ -0,0 +1,167 @@ +/* -*- 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 "BaseAccessibles.h" + +#include "LocalAccessible-inl.h" +#include "HyperTextAccessibleWrap.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "Role.h" +#include "States.h" +#include "nsIURI.h" + +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// LeafAccessible +//////////////////////////////////////////////////////////////////////////////// + +LeafAccessible::LeafAccessible(nsIContent* aContent, DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) { + mStateFlags |= eNoKidsFromDOM; +} + +//////////////////////////////////////////////////////////////////////////////// +// LeafAccessible: LocalAccessible public + +LocalAccessible* LeafAccessible::LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) { + // Don't walk into leaf accessibles. + return this; +} + +bool LeafAccessible::InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) { + MOZ_ASSERT_UNREACHABLE("InsertChildAt called on leaf accessible!"); + return false; +} + +bool LeafAccessible::RemoveChild(LocalAccessible* aChild) { + MOZ_ASSERT_UNREACHABLE("RemoveChild called on leaf accessible!"); + return false; +} + +bool LeafAccessible::IsAcceptableChild(nsIContent* aEl) const { + // No children for leaf accessible. + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// LinkableAccessible +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// LinkableAccessible. nsIAccessible + +void LinkableAccessible::TakeFocus() const { + if (const LocalAccessible* actionAcc = ActionWalk()) { + actionAcc->TakeFocus(); + } else { + AccessibleWrap::TakeFocus(); + } +} + +uint64_t LinkableAccessible::NativeLinkState() const { + bool isLink; + const LocalAccessible* actionAcc = ActionWalk(&isLink); + if (isLink) { + return states::LINKED | (actionAcc->LinkState() & states::TRAVERSED); + } + + return 0; +} + +void LinkableAccessible::Value(nsString& aValue) const { + aValue.Truncate(); + + LocalAccessible::Value(aValue); + if (!aValue.IsEmpty()) { + return; + } + + bool isLink; + const LocalAccessible* actionAcc = ActionWalk(&isLink); + if (isLink) { + actionAcc->Value(aValue); + } +} + +const LocalAccessible* LinkableAccessible::ActionWalk(bool* aIsLink, + bool* aIsOnclick) const { + if (aIsOnclick) { + *aIsOnclick = false; + } + if (aIsLink) { + *aIsLink = false; + } + + if (HasPrimaryAction()) { + if (aIsOnclick) { + *aIsOnclick = true; + } + + return nullptr; + } + + const Accessible* actionAcc = ActionAncestor(); + + const LocalAccessible* localAction = + actionAcc ? const_cast<Accessible*>(actionAcc)->AsLocal() : nullptr; + + if (!localAction) { + return nullptr; + } + + if (localAction->LinkState() & states::LINKED) { + if (aIsLink) { + *aIsLink = true; + } + } else if (aIsOnclick) { + *aIsOnclick = true; + } + + return localAction; +} + +KeyBinding LinkableAccessible::AccessKey() const { + if (const LocalAccessible* actionAcc = + const_cast<LinkableAccessible*>(this)->ActionWalk()) { + return actionAcc->AccessKey(); + } + + return LocalAccessible::AccessKey(); +} + +//////////////////////////////////////////////////////////////////////////////// +// LinkableAccessible: HyperLinkAccessible + +already_AddRefed<nsIURI> LinkableAccessible::AnchorURIAt( + uint32_t aAnchorIndex) const { + bool isLink; + const LocalAccessible* actionAcc = ActionWalk(&isLink); + if (isLink) { + NS_ASSERTION(actionAcc->IsLink(), "HyperLink isn't implemented."); + + if (actionAcc->IsLink()) { + return actionAcc->AnchorURIAt(aAnchorIndex); + } + } + + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// DummyAccessible +//////////////////////////////////////////////////////////////////////////////// + +uint64_t DummyAccessible::NativeState() const { return 0; } +uint64_t DummyAccessible::NativeInteractiveState() const { return 0; } + +uint64_t DummyAccessible::NativeLinkState() const { return 0; } + +bool DummyAccessible::NativelyUnavailable() const { return false; } + +void DummyAccessible::ApplyARIAState(uint64_t* aState) const {} diff --git a/accessible/generic/BaseAccessibles.h b/accessible/generic/BaseAccessibles.h new file mode 100644 index 0000000000..d3373ed2a5 --- /dev/null +++ b/accessible/generic/BaseAccessibles.h @@ -0,0 +1,139 @@ +/* -*- 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_BaseAccessibles_h__ +#define mozilla_a11y_BaseAccessibles_h__ + +#include "AccessibleWrap.h" +#include "HyperTextAccessibleWrap.h" + +class nsIContent; + +/** + * This file contains a number of classes that are used as base + * classes for the different accessibility implementations of + * the HTML and XUL widget sets. --jgaunt + */ + +namespace mozilla { +namespace a11y { + +/** + * Leaf version of DOM Accessible -- has no children + */ +class LeafAccessible : public AccessibleWrap { + public: + LeafAccessible(nsIContent* aContent, DocAccessible* aDoc); + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(LeafAccessible, AccessibleWrap) + + // LocalAccessible + virtual LocalAccessible* LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override; + bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) final; + bool RemoveChild(LocalAccessible* aChild) final; + + virtual bool IsAcceptableChild(nsIContent* aEl) const override; + + protected: + virtual ~LeafAccessible() {} +}; + +/** + * Used for text or image accessible nodes contained by link accessibles or + * accessibles for nodes with registered click event handler. It knows how to + * report the state of the host link (traveled or not) and can focus the host + * accessible programmatically. + */ +class LinkableAccessible : public AccessibleWrap { + public: + LinkableAccessible(nsIContent* aContent, DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) {} + + NS_INLINE_DECL_REFCOUNTING_INHERITED(LinkableAccessible, AccessibleWrap) + + // LocalAccessible + virtual void Value(nsString& aValue) const override; + virtual uint64_t NativeLinkState() const override; + virtual void TakeFocus() const override; + + // ActionAccessible + virtual KeyBinding AccessKey() const override; + + // ActionAccessible helpers + const LocalAccessible* ActionWalk(bool* aIsLink = nullptr, + bool* aIsOnclick = nullptr) const; + // HyperLinkAccessible + virtual already_AddRefed<nsIURI> AnchorURIAt( + uint32_t aAnchorIndex) const override; + + protected: + virtual ~LinkableAccessible() {} +}; + +/** + * A simple accessible that gets its enumerated role. + */ +template <a11y::role R> +class EnumRoleAccessible : public AccessibleWrap { + public: + EnumRoleAccessible(nsIContent* aContent, DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) {} + + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aPtr) override { + return LocalAccessible::QueryInterface(aIID, aPtr); + } + + // LocalAccessible + virtual a11y::role NativeRole() const override { return R; } + + protected: + virtual ~EnumRoleAccessible() {} +}; + +/** + * Like EnumRoleAccessible, but with text support. + */ +template <a11y::role R> +class EnumRoleHyperTextAccessible : public HyperTextAccessibleWrap { + public: + EnumRoleHyperTextAccessible(nsIContent* aContent, DocAccessible* aDoc) + : HyperTextAccessibleWrap(aContent, aDoc) {} + + // LocalAccessible + virtual a11y::role NativeRole() const override { return R; } + + protected: + virtual ~EnumRoleHyperTextAccessible() {} +}; + +/** + * A wrapper accessible around native accessible to connect it with + * crossplatform accessible tree. + */ +class DummyAccessible : public AccessibleWrap { + public: + explicit DummyAccessible(DocAccessible* aDocument = nullptr) + : AccessibleWrap(nullptr, aDocument) { + // IsDefunct() asserts if mContent is null, which is always true for + // DummyAccessible. We can prevent this by setting eSharedNode. + mStateFlags |= eSharedNode; + } + + uint64_t NativeState() const final; + uint64_t NativeInteractiveState() const final; + uint64_t NativeLinkState() const final; + bool NativelyUnavailable() const final; + void ApplyARIAState(uint64_t* aState) const final; + + protected: + virtual ~DummyAccessible() {} +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/DocAccessible-inl.h b/accessible/generic/DocAccessible-inl.h new file mode 100644 index 0000000000..493ca1fd21 --- /dev/null +++ b/accessible/generic/DocAccessible-inl.h @@ -0,0 +1,191 @@ +/* -*- 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_DocAccessible_inl_h_ +#define mozilla_a11y_DocAccessible_inl_h_ + +#include "DocAccessible.h" +#include "nsAccessibilityService.h" +#include "nsAccessiblePivot.h" +#include "NotificationController.h" +#include "States.h" +#include "nsIScrollableFrame.h" +#include "mozilla/dom/DocumentInlines.h" + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +namespace mozilla { +namespace a11y { + +inline LocalAccessible* DocAccessible::AccessibleOrTrueContainer( + nsINode* aNode, bool aNoContainerIfPruned) const { + // HTML comboboxes have no-content list accessible as an intermediate + // containing all options. + LocalAccessible* container = + GetAccessibleOrContainer(aNode, aNoContainerIfPruned); + if (container && container->IsHTMLCombobox()) { + return container->LocalFirstChild(); + } + return container; +} + +inline nsIAccessiblePivot* DocAccessible::VirtualCursor() { + if (!mVirtualCursor) { + mVirtualCursor = new nsAccessiblePivot(this); + mVirtualCursor->AddObserver(this); + } + return mVirtualCursor; +} + +inline bool DocAccessible::IsContentLoaded() const { + // eDOMLoaded flag check is used for error pages as workaround to make this + // method return correct result since error pages do not receive 'pageshow' + // event and as consequence Document::IsShowing() returns false. + return mDocumentNode && mDocumentNode->IsVisible() && + (mDocumentNode->IsShowing() || HasLoadState(eDOMLoaded)); +} + +inline bool DocAccessible::IsHidden() const { return mDocumentNode->Hidden(); } + +inline void DocAccessible::FireDelayedEvent(AccEvent* aEvent) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) logging::DocLoadEventFired(aEvent); +#endif + + mNotificationController->QueueEvent(aEvent); +} + +inline void DocAccessible::FireDelayedEvent(uint32_t aEventType, + LocalAccessible* aTarget) { + RefPtr<AccEvent> event = new AccEvent(aEventType, aTarget); + FireDelayedEvent(event); +} + +inline void DocAccessible::BindChildDocument(DocAccessible* aDocument) { + mNotificationController->ScheduleChildDocBinding(aDocument); +} + +template <class Class, class... Args> +inline void DocAccessible::HandleNotification( + Class* aInstance, typename TNotification<Class, Args...>::Callback aMethod, + Args*... aArgs) { + if (mNotificationController) { + mNotificationController->HandleNotification<Class, Args...>( + aInstance, aMethod, aArgs...); + } +} + +inline void DocAccessible::UpdateText(nsIContent* aTextNode) { + NS_ASSERTION(mNotificationController, "The document was shut down!"); + + // Ignore the notification if initial tree construction hasn't been done yet. + if (mNotificationController && HasLoadState(eTreeConstructed)) { + mNotificationController->ScheduleTextUpdate(aTextNode); + } +} + +inline void DocAccessible::NotifyOfLoad(uint32_t aLoadEventType) { + mLoadState |= eDOMLoaded; + mLoadEventType = aLoadEventType; + + // If the document is loaded completely then network activity was presumingly + // caused by file loading. Fire busy state change event. + if (HasLoadState(eCompletelyLoaded) && IsLoadEventTarget()) { + RefPtr<AccEvent> stateEvent = + new AccStateChangeEvent(this, states::BUSY, false); + FireDelayedEvent(stateEvent); + } +} + +inline void DocAccessible::MaybeNotifyOfValueChange( + LocalAccessible* aAccessible) { + if (aAccessible->IsCombobox() || aAccessible->Role() == roles::ENTRY || + aAccessible->Role() == roles::SPINBUTTON) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, aAccessible); + } +} + +inline LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMapOrContainer( + nsINode* aNode) const { + LocalAccessible* acc = GetAccessibleEvenIfNotInMap(aNode); + return acc ? acc : GetContainerAccessible(aNode); +} + +inline void DocAccessible::CreateSubtree(LocalAccessible* aChild) { + // If a focused node has been shown then it could mean its frame was recreated + // while the node stays focused and we need to fire focus event on + // the accessible we just created. If the queue contains a focus event for + // this node already then it will be suppressed by this one. + LocalAccessible* focusedAcc = nullptr; + CacheChildrenInSubtree(aChild, &focusedAcc); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eVerbose)) { + logging::Tree("TREE", "Created subtree", aChild); + } +#endif + + // Fire events for ARIA elements. + if (aChild->HasARIARole()) { + roles::Role role = aChild->ARIARole(); + if (role == roles::MENUPOPUP) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, aChild); + } else if (role == roles::ALERT) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, aChild); + } + } + + // XXX: do we really want to send focus to focused DOM node not taking into + // account active item? + if (focusedAcc) { + FocusMgr()->DispatchFocusEvent(this, focusedAcc); + SelectionMgr()->SetControlSelectionListener( + focusedAcc->GetNode()->AsElement()); + } +} + +inline DocAccessible::AttrRelProviders* DocAccessible::GetRelProviders( + dom::Element* aElement, const nsAString& aID) const { + DependentIDsHashtable* hash = mDependentIDsHashes.Get( + aElement->GetUncomposedDocOrConnectedShadowRoot()); + if (hash) { + return hash->Get(aID); + } + return nullptr; +} + +inline DocAccessible::AttrRelProviders* DocAccessible::GetOrCreateRelProviders( + dom::Element* aElement, const nsAString& aID) { + dom::DocumentOrShadowRoot* docOrShadowRoot = + aElement->GetUncomposedDocOrConnectedShadowRoot(); + DependentIDsHashtable* hash = + mDependentIDsHashes.GetOrInsertNew(docOrShadowRoot); + + return hash->GetOrInsertNew(aID); +} + +inline void DocAccessible::RemoveRelProvidersIfEmpty(dom::Element* aElement, + const nsAString& aID) { + dom::DocumentOrShadowRoot* docOrShadowRoot = + aElement->GetUncomposedDocOrConnectedShadowRoot(); + DependentIDsHashtable* hash = mDependentIDsHashes.Get(docOrShadowRoot); + if (hash) { + AttrRelProviders* providers = hash->Get(aID); + if (providers && providers->Length() == 0) { + hash->Remove(aID); + if (mDependentIDsHashes.IsEmpty()) { + mDependentIDsHashes.Remove(docOrShadowRoot); + } + } + } +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp new file mode 100644 index 0000000000..42747aee46 --- /dev/null +++ b/accessible/generic/DocAccessible.cpp @@ -0,0 +1,2800 @@ +/* -*- 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 "LocalAccessible-inl.h" +#include "AccIterator.h" +#include "AccAttributes.h" +#include "CachedTableAccessible.h" +#include "DocAccessible-inl.h" +#include "DocAccessibleChild.h" +#include "EventTree.h" +#include "HTMLImageMapAccessible.h" +#include "mozilla/ProfilerMarkers.h" +#include "nsAccCache.h" +#include "nsAccessiblePivot.h" +#include "nsAccUtils.h" +#include "nsEventShell.h" +#include "nsIIOService.h" +#include "nsLayoutUtils.h" +#include "nsTextEquivUtils.h" +#include "Pivot.h" +#include "Role.h" +#include "RootAccessible.h" +#include "TreeWalker.h" +#include "xpcAccessibleDocument.h" + +#include "nsCommandManager.h" +#include "nsContentUtils.h" +#include "nsIDocShell.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "nsIContentInlines.h" +#include "nsIEditingSession.h" +#include "nsIFrame.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsImageFrame.h" +#include "nsViewManager.h" +#include "nsIScrollableFrame.h" +#include "nsUnicharUtils.h" +#include "nsIURI.h" +#include "nsIWebNavigation.h" +#include "nsFocusManager.h" +#include "nsTHashSet.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Components.h" // for mozilla::components +#include "mozilla/EditorBase.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/ipc/ProcessChild.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/dom/AncestorIterator.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/DocumentType.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/UserActivation.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Static member initialization + +static nsStaticAtom* const kRelationAttrs[] = {nsGkAtoms::aria_labelledby, + nsGkAtoms::aria_describedby, + nsGkAtoms::aria_details, + nsGkAtoms::aria_owns, + nsGkAtoms::aria_controls, + nsGkAtoms::aria_flowto, + nsGkAtoms::aria_errormessage, + nsGkAtoms::_for, + nsGkAtoms::control}; + +static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs); + +//////////////////////////////////////////////////////////////////////////////// +// Constructor/desctructor + +DocAccessible::DocAccessible(dom::Document* aDocument, + PresShell* aPresShell) + : // XXX don't pass a document to the LocalAccessible constructor so that + // we don't set mDoc until our vtable is fully setup. If we set mDoc + // before setting up the vtable we will call LocalAccessible::AddRef() + // but not the overrides of it for subclasses. It is important to call + // those overrides to avoid confusing leak checking machinary. + HyperTextAccessibleWrap(nullptr, nullptr), + // XXX aaronl should we use an algorithm for the initial cache size? + mAccessibleCache(kDefaultCacheLength), + mNodeToAccessibleMap(kDefaultCacheLength), + mDocumentNode(aDocument), + mLoadState(eTreeConstructionPending), + mDocFlags(0), + mViewportCacheDirty(false), + mLoadEventType(0), + mPrevStateBits(0), + mVirtualCursor(nullptr), + mPresShell(aPresShell), + mIPCDoc(nullptr) { + mGenericTypes |= eDocument; + mStateFlags |= eNotNodeMapEntry; + mDoc = this; + + MOZ_ASSERT(mPresShell, "should have been given a pres shell"); + mPresShell->SetDocAccessible(this); +} + +DocAccessible::~DocAccessible() { + NS_ASSERTION(!mPresShell, "LastRelease was never called!?!"); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, + LocalAccessible) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVirtualCursor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments) + for (const auto& hashEntry : tmp->mDependentIDsHashes.Values()) { + for (const auto& providers : hashEntry->Values()) { + for (int32_t provIdx = providers->Length() - 1; provIdx >= 0; provIdx--) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( + cb, "content of dependent ids hash entry of document accessible"); + + const auto& provider = (*providers)[provIdx]; + cb.NoteXPCOMChild(provider->mContent); + } + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingUpdates) + for (const auto& ar : tmp->mARIAOwnsHash.Values()) { + for (uint32_t i = 0; i < ar->Length(); i++) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item"); + cb.NoteXPCOMChild(ar->ElementAt(i)); + } + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mVirtualCursor) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments) + tmp->mDependentIDsHashes.Clear(); + tmp->mNodeToAccessibleMap.Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingUpdates) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE + tmp->mARIAOwnsHash.Clear(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible) + NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivotObserver) +NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible) + +NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible) +NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible) + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessible + +ENameValueFlag DocAccessible::Name(nsString& aName) const { + aName.Truncate(); + + if (mParent) { + mParent->Name(aName); // Allow owning iframe to override the name + } + if (aName.IsEmpty()) { + // Allow name via aria-labelledby or title attribute + LocalAccessible::Name(aName); + } + if (aName.IsEmpty()) { + Title(aName); // Try title element + } + if (aName.IsEmpty()) { // Last resort: use URL + URL(aName); + } + + return eNameOK; +} + +// LocalAccessible public method +role DocAccessible::NativeRole() const { + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode); + if (docShell) { + nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot; + docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot)); + int32_t itemType = docShell->ItemType(); + if (sameTypeRoot == docShell) { + // Root of content or chrome tree + if (itemType == nsIDocShellTreeItem::typeChrome) { + return roles::CHROME_WINDOW; + } + + if (itemType == nsIDocShellTreeItem::typeContent) { + return roles::DOCUMENT; + } + } else if (itemType == nsIDocShellTreeItem::typeContent) { + return roles::DOCUMENT; + } + } + + return roles::PANE; // Fall back; +} + +void DocAccessible::Description(nsString& aDescription) const { + if (mParent) mParent->Description(aDescription); + + if (HasOwnContent() && aDescription.IsEmpty()) { + nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, + aDescription); + } +} + +// LocalAccessible public method +uint64_t DocAccessible::NativeState() const { + // Document is always focusable. + uint64_t state = + states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl + if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED; + + // Expose stale state until the document is ready (DOM is loaded and tree is + // constructed). + if (!HasLoadState(eReady)) state |= states::STALE; + + // Expose state busy until the document and all its subdocuments is completely + // loaded. + if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY; + + nsIFrame* frame = GetFrame(); + if (!frame || !frame->IsVisibleConsideringAncestors( + nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) { + state |= states::INVISIBLE | states::OFFSCREEN; + } + + RefPtr<EditorBase> editorBase = GetEditor(); + state |= editorBase ? states::EDITABLE : states::READONLY; + + return state; +} + +uint64_t DocAccessible::NativeInteractiveState() const { + // Document is always focusable. + return states::FOCUSABLE; +} + +bool DocAccessible::NativelyUnavailable() const { return false; } + +// LocalAccessible public method +void DocAccessible::ApplyARIAState(uint64_t* aState) const { + // Grab states from content element. + if (mContent) LocalAccessible::ApplyARIAState(aState); + + // Allow iframe/frame etc. to have final state override via ARIA. + if (mParent) mParent->ApplyARIAState(aState); +} + +Accessible* DocAccessible::FocusedChild() { + // Return an accessible for the current global focus, which does not have to + // be contained within the current document. + return FocusMgr()->FocusedAccessible(); +} + +void DocAccessible::TakeFocus() const { + // Focus the document. + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + RefPtr<dom::Element> newFocus; + dom::AutoHandlingUserInputStatePusher inputStatePusher(true); + fm->MoveFocus(mDocumentNode->GetWindow(), nullptr, + nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus)); +} + +// HyperTextAccessible method +already_AddRefed<EditorBase> DocAccessible::GetEditor() const { + // Check if document is editable (designMode="on" case). Otherwise check if + // the html:body (for HTML document case) or document element is editable. + if (!mDocumentNode->IsInDesignMode() && + (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) { + return nullptr; + } + + nsCOMPtr<nsIDocShell> docShell = mDocumentNode->GetDocShell(); + if (!docShell) { + return nullptr; + } + + nsCOMPtr<nsIEditingSession> editingSession; + docShell->GetEditingSession(getter_AddRefs(editingSession)); + if (!editingSession) return nullptr; // No editing session interface + + RefPtr<HTMLEditor> htmlEditor = + editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow()); + if (!htmlEditor) { + return nullptr; + } + + bool isEditable = false; + htmlEditor->GetIsDocumentEditable(&isEditable); + if (isEditable) { + return htmlEditor.forget(); + } + + return nullptr; +} + +// DocAccessible public method + +void DocAccessible::URL(nsAString& aURL) const { + aURL.Truncate(); + nsCOMPtr<nsISupports> container = mDocumentNode->GetContainer(); + nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container)); + if (MOZ_UNLIKELY(!webNav)) { + return; + } + + nsCOMPtr<nsIURI> uri; + webNav->GetCurrentURI(getter_AddRefs(uri)); + if (MOZ_UNLIKELY(!uri)) { + return; + } + // Let's avoid treating too long URI in the main process for avoiding + // memory fragmentation as far as possible. + if (uri->SchemeIs("data") || uri->SchemeIs("blob")) { + return; + } + + nsCOMPtr<nsIIOService> io = mozilla::components::IO::Service(); + if (NS_WARN_IF(!io)) { + return; + } + nsCOMPtr<nsIURI> exposableURI; + if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) || + MOZ_UNLIKELY(!exposableURI)) { + return; + } + nsAutoCString theURL; + if (NS_SUCCEEDED(exposableURI->GetSpec(theURL))) { + CopyUTF8toUTF16(theURL, aURL); + } +} + +void DocAccessible::Title(nsString& aTitle) const { + mDocumentNode->GetTitle(aTitle); +} + +void DocAccessible::MimeType(nsAString& aType) const { + mDocumentNode->GetContentType(aType); +} + +void DocAccessible::DocType(nsAString& aType) const { + dom::DocumentType* docType = mDocumentNode->GetDoctype(); + if (docType) docType->GetPublicId(aType); +} + +void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc, + uint64_t aNewDomain) { + if (!mIPCDoc || !StaticPrefs::accessibility_cache_enabled_AtStartup()) { + return; + } + uint64_t& domain = mQueuedCacheUpdates.LookupOrInsert(aAcc, 0); + domain |= aNewDomain; + Controller()->ScheduleProcessing(); +} + +void DocAccessible::QueueCacheUpdateForDependentRelations( + LocalAccessible* aAcc) { + if (!mIPCDoc || !StaticPrefs::accessibility_cache_enabled_AtStartup() || + !aAcc || !aAcc->Elm() || !aAcc->IsInDocument() || aAcc->IsDefunct()) { + return; + } + nsAutoString ID; + aAcc->DOMNodeID(ID); + if (AttrRelProviders* list = GetRelProviders(aAcc->Elm(), ID)) { + // We call this function when we've noticed an ID change, or when an acc + // is getting bound to its document. We need to ensure any existing accs + // that depend on this acc's ID have their rel cache entries updated. + for (const auto& provider : *list) { + LocalAccessible* relatedAcc = GetAccessible(provider->mContent); + if (!relatedAcc || relatedAcc->IsDefunct() || + !relatedAcc->IsInDocument() || + mInsertedAccessibles.Contains(relatedAcc)) { + continue; + } + QueueCacheUpdate(relatedAcc, CacheDomain::Relations); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +void DocAccessible::Init() { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) { + logging::DocCreate("document initialize", mDocumentNode, this); + } +#endif + + // Initialize notification controller. + mNotificationController = new NotificationController(this, mPresShell); + + // Mark the DocAccessible as loaded if its DOM document is already loaded at + // this point. This can happen for one of three reasons: + // 1. A11y was started late. + // 2. DOM loading for a document (probably an in-process iframe) completed + // before its Accessible container was created. + // 3. The PresShell for the document was created after DOM loading completed. + // In that case, we tried to create the DocAccessible when DOM loading + // completed, but we can't create a DocAccessible without a PresShell, so + // this failed. The DocAccessible was subsequently created due to a layout + // notification. + if (mDocumentNode->GetReadyStateEnum() == + dom::Document::READYSTATE_COMPLETE) { + mLoadState |= eDOMLoaded; + // If this happened due to reasons 1 or 2, it isn't *necessary* to fire a + // doc load complete event. If it happened due to reason 3, we need to fire + // doc load complete because clients (especially tests) might be waiting + // for the document to load using this event. We can't distinguish why this + // happened at this point, so just fire it regardless. It won't do any + // harm even if it isn't necessary. We set mLoadEventType here and it will + // be fired in ProcessLoad as usual. + mLoadEventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE; + } else if (mDocumentNode->IsInitialDocument()) { + // The initial about:blank document will never finish loading, so we can + // immediately mark it loaded to avoid waiting for its load. + mLoadState |= eDOMLoaded; + } + + AddEventListeners(); +} + +void DocAccessible::Shutdown() { + if (!mPresShell) { // already shutdown + return; + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) { + logging::DocDestroy("document shutdown", mDocumentNode, this); + } +#endif + + // Mark the document as shutdown before AT is notified about the document + // removal from its container (valid for root documents on ATK and due to + // some reason for MSAA, refer to bug 757392 for details). + mStateFlags |= eIsDefunct; + + if (mNotificationController) { + mNotificationController->Shutdown(); + mNotificationController = nullptr; + } + + RemoveEventListeners(); + + // mParent->RemoveChild clears mParent, but we need to know whether we were a + // child later, so use a flag. + const bool isChild = !!mParent; + if (mParent) { + DocAccessible* parentDocument = mParent->Document(); + if (parentDocument) parentDocument->RemoveChildDocument(this); + + mParent->RemoveChild(this); + MOZ_ASSERT(!mParent, "Parent has to be null!"); + } + + mPresShell->SetDocAccessible(nullptr); + mPresShell = nullptr; // Avoid reentrancy + + // Walk the array backwards because child documents remove themselves from the + // array as they are shutdown. + int32_t childDocCount = mChildDocuments.Length(); + for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { + mChildDocuments[idx]->Shutdown(); + } + + mChildDocuments.Clear(); + // mQueuedCacheUpdates can contain a reference to this document (ex. if the + // doc is scrollable and we're sending a scroll position update). Clear the + // map here to avoid creating ref cycles. + mQueuedCacheUpdates.Clear(); + + // XXX thinking about ordering? + if (mIPCDoc) { + MOZ_ASSERT(IPCAccessibilityActive()); + mIPCDoc->Shutdown(); + MOZ_ASSERT(!mIPCDoc); + } + + if (mVirtualCursor) { + mVirtualCursor->RemoveObserver(this); + mVirtualCursor = nullptr; + } + + mDependentIDsHashes.Clear(); + mNodeToAccessibleMap.Clear(); + + mAnchorJumpElm = nullptr; + mInvalidationList.Clear(); + mPendingUpdates.Clear(); + + for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) { + LocalAccessible* accessible = iter.Data(); + MOZ_ASSERT(accessible); + if (accessible) { + // This might have been focused with FocusManager::ActiveItemChanged. In + // that case, we must notify FocusManager so that it clears the active + // item. Otherwise, it will hold on to a defunct Accessible. Normally, + // this happens in UnbindFromDocument, but we don't call that when the + // whole document shuts down. + if (FocusMgr()->WasLastFocused(accessible)) { + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("doc shutdown", accessible); + } +#endif + } + if (!accessible->IsDefunct()) { + // Unlink parent to avoid its cleaning overhead in shutdown. + accessible->mParent = nullptr; + accessible->Shutdown(); + } + } + iter.Remove(); + } + + HyperTextAccessibleWrap::Shutdown(); + + MOZ_ASSERT(GetAccService()); + GetAccService()->NotifyOfDocumentShutdown( + this, mDocumentNode, + // Make sure we don't shut down AccService while a parent document is + // still shutting down. The parent will allow service shutdown when it + // reaches this point. + /* aAllowServiceShutdown */ !isChild); + mDocumentNode = nullptr; +} + +nsIFrame* DocAccessible::GetFrame() const { + nsIFrame* root = nullptr; + if (mPresShell) { + root = mPresShell->GetRootFrame(); + } + + return root; +} + +nsINode* DocAccessible::GetNode() const { return mDocumentNode; } + +// DocAccessible protected member +nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const { + *aRelativeFrame = GetFrame(); + + dom::Document* document = mDocumentNode; + dom::Document* parentDoc = nullptr; + + nsRect bounds; + while (document) { + PresShell* presShell = document->GetPresShell(); + if (!presShell) { + return nsRect(); + } + + nsRect scrollPort; + nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable(); + if (sf) { + scrollPort = sf->GetScrollPortRect(); + } else { + nsIFrame* rootFrame = presShell->GetRootFrame(); + if (!rootFrame) return nsRect(); + + scrollPort = rootFrame->GetRect(); + } + + if (parentDoc) { // After first time thru loop + // XXXroc bogus code! scrollPort is relative to the viewport of + // this document, but we're intersecting rectangles derived from + // multiple documents and assuming they're all in the same coordinate + // system. See bug 514117. + bounds.IntersectRect(scrollPort, bounds); + } else { // First time through loop + bounds = scrollPort; + } + + document = parentDoc = document->GetInProcessParentDocument(); + } + + return bounds; +} + +// DocAccessible protected member +nsresult DocAccessible::AddEventListeners() { + SelectionMgr()->AddDocSelectionListener(mPresShell); + + // Add document observer. + mDocumentNode->AddObserver(this); + return NS_OK; +} + +// DocAccessible protected member +nsresult DocAccessible::RemoveEventListeners() { + // Remove listeners associated with content documents + NS_ASSERTION(mDocumentNode, "No document during removal of listeners."); + + if (mDocumentNode) { + mDocumentNode->RemoveObserver(this); + } + + if (mScrollWatchTimer) { + mScrollWatchTimer->Cancel(); + mScrollWatchTimer = nullptr; + NS_RELEASE_THIS(); // Kung fu death grip + } + + SelectionMgr()->RemoveDocSelectionListener(mPresShell); + return NS_OK; +} + +void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) { + DocAccessible* docAcc = reinterpret_cast<DocAccessible*>(aClosure); + + if (docAcc) { + // Dispatch a scroll-end for all entries in table. They have not + // been scrolled in at least `kScrollEventInterval`. + for (auto iter = docAcc->mLastScrollingDispatch.Iter(); !iter.Done(); + iter.Next()) { + docAcc->DispatchScrollingEvent(iter.Key(), + nsIAccessibleEvent::EVENT_SCROLLING_END); + iter.Remove(); + } + + if (docAcc->mScrollWatchTimer) { + docAcc->mScrollWatchTimer = nullptr; + NS_RELEASE(docAcc); // Release kung fu death grip + } + } +} + +void DocAccessible::HandleScroll(nsINode* aTarget) { + nsINode* target = aTarget; + LocalAccessible* targetAcc = GetAccessible(target); + if (!targetAcc && target->IsInNativeAnonymousSubtree()) { + // The scroll event for textareas comes from a native anonymous div. We need + // the closest non-anonymous ancestor to get the right Accessible. + target = target->GetClosestNativeAnonymousSubtreeRootParent(); + targetAcc = GetAccessible(target); + } + // Regardless of our scroll timer, we need to send a cache update + // to ensure the next Bounds() query accurately reflects our position + // after scrolling. + if (targetAcc) { + QueueCacheUpdate(targetAcc, CacheDomain::ScrollPosition); + } + + const uint32_t kScrollEventInterval = 100; + // If we haven't dispatched a scrolling event for a target in at least + // kScrollEventInterval milliseconds, dispatch one now. + mLastScrollingDispatch.WithEntryHandle(target, [&](auto&& lastDispatch) { + const TimeStamp now = TimeStamp::Now(); + + if (!lastDispatch || + (now - lastDispatch.Data()).ToMilliseconds() >= kScrollEventInterval) { + // We can't fire events on a document whose tree isn't constructed yet. + if (HasLoadState(eTreeConstructed)) { + DispatchScrollingEvent(target, nsIAccessibleEvent::EVENT_SCROLLING); + } + lastDispatch.InsertOrUpdate(now); + } + }); + + // If timer callback is still pending, push it 100ms into the future. + // When scrolling ends and we don't fire this callback anymore, the + // timer callback will fire and dispatch an EVENT_SCROLLING_END. + if (mScrollWatchTimer) { + mScrollWatchTimer->SetDelay(kScrollEventInterval); + } else { + NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer), + ScrollTimerCallback, this, kScrollEventInterval, + nsITimer::TYPE_ONE_SHOT, + "a11y::DocAccessible::ScrollPositionDidChange"); + if (mScrollWatchTimer) { + NS_ADDREF_THIS(); // Kung fu death grip + } + } +} + +std::pair<nsPoint, nsRect> DocAccessible::ComputeScrollData( + LocalAccessible* aAcc) { + nsPoint scrollPoint; + nsRect scrollRange; + + if (nsIFrame* frame = aAcc->GetFrame()) { + nsIScrollableFrame* sf = aAcc == this + ? mPresShell->GetRootScrollFrameAsScrollable() + : frame->GetScrollTargetFrame(); + + // If there is no scrollable frame, it's likely a scroll in a popup, like + // <select>. Return a scroll offset and range of 0. The scroll info + // is currently only used on Android, and popups are rendered natively + // there. + if (sf) { + scrollPoint = sf->GetScrollPosition() * mPresShell->GetResolution(); + scrollRange = sf->GetScrollRange(); + scrollRange.ScaleRoundOut(mPresShell->GetResolution()); + } + } + + return {scrollPoint, scrollRange}; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessiblePivotObserver + +NS_IMETHODIMP +DocAccessible::OnPivotChanged(nsIAccessiblePivot* aPivot, + nsIAccessible* aOldAccessible, int32_t aOldStart, + int32_t aOldEnd, nsIAccessible* aNewAccessible, + int32_t aNewStart, int32_t aNewEnd, + PivotMoveReason aReason, + TextBoundaryType aBoundaryType, + bool aIsFromUserInput) { + RefPtr<AccEvent> event = new AccVCChangeEvent( + this, (aOldAccessible ? aOldAccessible->ToInternalAccessible() : nullptr), + aOldStart, aOldEnd, + (aNewAccessible ? aNewAccessible->ToInternalAccessible() : nullptr), + aNewStart, aNewEnd, aReason, aBoundaryType, + aIsFromUserInput ? eFromUserInput : eNoUserInput); + nsEventShell::FireEvent(event); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIDocumentObserver + +NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible) +NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible) + +void DocAccessible::AttributeWillChange(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + LocalAccessible* accessible = GetAccessible(aElement); + if (!accessible) { + if (aElement != mContent) return; + + accessible = this; + } + + // Update dependent IDs cache. Take care of elements that are accessible + // because dependent IDs cache doesn't contain IDs from non accessible + // elements. + if (aModType != dom::MutationEvent_Binding::ADDITION) { + RemoveDependentIDsFor(accessible, aAttribute); + } + + if (aAttribute == nsGkAtoms::id) { + if (accessible->IsActiveDescendant()) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::ACTIVE, false); + FireDelayedEvent(event); + } + + RelocateARIAOwnedIfNeeded(aElement); + } + + if (aAttribute == nsGkAtoms::aria_activedescendant) { + if (LocalAccessible* activeDescendant = accessible->CurrentItem()) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(activeDescendant, states::ACTIVE, false); + FireDelayedEvent(event); + } + } + + // If attribute affects accessible's state, store the old state so we can + // later compare it against the state of the accessible after the attribute + // change. + if (accessible->AttributeChangesState(aAttribute)) { + mPrevStateBits = accessible->State(); + } else { + mPrevStateBits = 0; + } +} + +void DocAccessible::NativeAnonymousChildListChange(nsIContent* aContent, + bool aIsRemove) { + if (aIsRemove) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "Anonymous content removed; doc: %p", this); + logging::Node("node", aContent); + logging::MsgEnd(); + } +#endif + + ContentRemoved(aContent); + } +} + +void DocAccessible::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + NS_ASSERTION(!IsDefunct(), + "Attribute changed called on defunct document accessible!"); + + // Proceed even if the element is not accessible because element may become + // accessible if it gets certain attribute. + if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) return; + + // Update the accessible tree on aria-hidden change. Make sure to not create + // a tree under aria-hidden='true'. + if (aAttribute == nsGkAtoms::aria_hidden) { + if (aria::HasDefinedARIAHidden(aElement)) { + ContentRemoved(aElement); + } else { + ContentInserted(aElement, aElement->GetNextSibling()); + } + return; + } + + LocalAccessible* accessible = GetAccessible(aElement); + if (!accessible) { + if (mContent == aElement) { + // The attribute change occurred on the root content of this + // DocAccessible, so handle it as an attribute change on this. + accessible = this; + } else { + if (aModType == dom::MutationEvent_Binding::ADDITION && + aria::AttrCharacteristicsFor(aAttribute) & ATTR_GLOBAL) { + // The element doesn't have an Accessible, but a global ARIA attribute + // was just added, which means we should probably create an Accessible. + ContentInserted(aElement, aElement->GetNextSibling()); + return; + } + // The element doesn't have an Accessible, so ignore the attribute + // change. + return; + } + } + + MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(), + "DOM attribute change on an accessible detached from the tree"); + + if (aAttribute == nsGkAtoms::id) { + dom::Element* elm = accessible->Elm(); + RelocateARIAOwnedIfNeeded(elm); + ARIAActiveDescendantIDMaybeMoved(accessible); + accessible->SendCache(CacheDomain::DOMNodeID, CacheUpdateType::Update); + QueueCacheUpdateForDependentRelations(accessible); + } + + // The activedescendant universal property redirects accessible focus events + // to the element with the id that activedescendant points to. Make sure + // the tree up to date before processing. In other words, when a node has just + // been inserted, the tree won't be up to date yet, so we must always schedule + // an async notification so that a newly inserted node will be present in + // the tree. + if (aAttribute == nsGkAtoms::aria_activedescendant) { + mNotificationController + ->ScheduleNotification<DocAccessible, LocalAccessible>( + this, &DocAccessible::ARIAActiveDescendantChanged, accessible); + return; + } + + // Defer to accessible any needed actions like changing states or emiting + // events. + accessible->DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue, + mPrevStateBits); + + // Update dependent IDs cache. We handle elements with accessibles. + // If the accessible or element with the ID doesn't exist yet the cache will + // be updated when they are added. + if (aModType == dom::MutationEvent_Binding::MODIFICATION || + aModType == dom::MutationEvent_Binding::ADDITION) { + AddDependentIDsFor(accessible, aAttribute); + } +} + +void DocAccessible::ARIAAttributeDefaultWillChange(dom::Element* aElement, + nsAtom* aAttribute, + int32_t aModType) { + NS_ASSERTION(!IsDefunct(), + "Attribute changed called on defunct document accessible!"); + + if (aElement->HasAttr(aAttribute)) { + return; + } + + AttributeWillChange(aElement, kNameSpaceID_None, aAttribute, aModType); +} + +void DocAccessible::ARIAAttributeDefaultChanged(dom::Element* aElement, + nsAtom* aAttribute, + int32_t aModType) { + NS_ASSERTION(!IsDefunct(), + "Attribute changed called on defunct document accessible!"); + + if (aElement->HasAttr(aAttribute)) { + return; + } + + AttributeChanged(aElement, kNameSpaceID_None, aAttribute, aModType, nullptr); +} + +void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) { + if (dom::Element* elm = aAccessible->Elm()) { + nsAutoString id; + if (elm->GetAttr(nsGkAtoms::aria_activedescendant, id)) { + dom::Element* activeDescendantElm = IDRefsIterator::GetElem(elm, id); + if (activeDescendantElm) { + LocalAccessible* activeDescendant = GetAccessible(activeDescendantElm); + if (activeDescendant) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(activeDescendant, states::ACTIVE, true); + FireDelayedEvent(event); + if (aAccessible->IsActiveWidget()) { + FocusMgr()->ActiveItemChanged(activeDescendant, false); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("ARIA activedescedant changed", + activeDescendant); + } +#endif + } + return; + } + } + } + + // aria-activedescendant was cleared or changed to a non-existent node. + // Move focus back to the element itself. + FocusMgr()->ActiveItemChanged(aAccessible, false); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("ARIA activedescedant cleared", + aAccessible); + } +#endif + } +} + +void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) { + MaybeHandleChangeToHiddenNameOrDescription(aFirstNewContent); +} + +void DocAccessible::ElementStateChanged(dom::Document* aDocument, + dom::Element* aElement, + dom::ElementState aStateMask) { + if (aStateMask.HasState(dom::ElementState::READWRITE) && + aElement == mDocumentNode->GetRootElement()) { + // This handles changes to designMode. contentEditable is handled by + // LocalAccessible::AttributeChangesState and + // LocalAccessible::DOMAttributeChanged. + const bool isEditable = + aElement->State().HasState(dom::ElementState::READWRITE); + RefPtr<AccEvent> event = + new AccStateChangeEvent(this, states::EDITABLE, isEditable); + FireDelayedEvent(event); + event = new AccStateChangeEvent(this, states::READONLY, !isEditable); + FireDelayedEvent(event); + } + + LocalAccessible* accessible = GetAccessible(aElement); + if (!accessible) return; + + if (aStateMask.HasState(dom::ElementState::CHECKED)) { + LocalAccessible* widget = accessible->ContainerWidget(); + if (widget && widget->IsSelect()) { + // Changing selection here changes what we cache for + // the viewport. + SetViewportCacheDirty(true); + AccSelChangeEvent::SelChangeType selChangeType = + aElement->State().HasState(dom::ElementState::CHECKED) + ? AccSelChangeEvent::eSelectionAdd + : AccSelChangeEvent::eSelectionRemove; + RefPtr<AccEvent> event = + new AccSelChangeEvent(widget, accessible, selChangeType); + FireDelayedEvent(event); + return; + } + + RefPtr<AccEvent> event = new AccStateChangeEvent( + accessible, states::CHECKED, + aElement->State().HasState(dom::ElementState::CHECKED)); + FireDelayedEvent(event); + } + + if (aStateMask.HasState(dom::ElementState::INVALID)) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::INVALID, true); + FireDelayedEvent(event); + } + + if (aStateMask.HasState(dom::ElementState::REQUIRED)) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::REQUIRED); + FireDelayedEvent(event); + } + + if (aStateMask.HasState(dom::ElementState::VISITED)) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::TRAVERSED, true); + FireDelayedEvent(event); + } + + // We only expose dom::ElementState::DEFAULT on buttons, but we can get + // notifications for other controls like checkboxes. + if (aStateMask.HasState(dom::ElementState::DEFAULT) && + accessible->IsButton()) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::DEFAULT); + FireDelayedEvent(event); + } +} + +void DocAccessible::CharacterDataWillChange(nsIContent* aContent, + const CharacterDataChangeInfo&) {} + +void DocAccessible::CharacterDataChanged(nsIContent* aContent, + const CharacterDataChangeInfo&) {} + +void DocAccessible::ContentInserted(nsIContent* aChild) { + MaybeHandleChangeToHiddenNameOrDescription(aChild); +} + +void DocAccessible::ContentRemoved(nsIContent* aChildNode, + nsIContent* aPreviousSiblingNode) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "DOM content removed; doc: %p", this); + logging::Node("container node", aChildNode->GetParent()); + logging::Node("content node", aChildNode); + logging::MsgEnd(); + } +#endif + // This one and content removal notification from layout may result in + // double processing of same subtrees. If it pops up in profiling, then + // consider reusing a document node cache to reject these notifications early. + ContentRemoved(aChildNode); +} + +void DocAccessible::ParentChainChanged(nsIContent* aContent) {} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +#ifdef A11Y_LOG +nsresult DocAccessible::HandleAccEvent(AccEvent* aEvent) { + if (logging::IsEnabled(logging::eDocLoad)) { + logging::DocLoadEventHandled(aEvent); + } + + return HyperTextAccessible::HandleAccEvent(aEvent); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Public members + +nsPresContext* DocAccessible::PresContext() const { + return mPresShell->GetPresContext(); +} + +void* DocAccessible::GetNativeWindow() const { + if (!mPresShell) { + return nullptr; + } + + nsViewManager* vm = mPresShell->GetViewManager(); + if (!vm) return nullptr; + + nsCOMPtr<nsIWidget> widget = vm->GetRootWidget(); + if (widget) return widget->GetNativeData(NS_NATIVE_WINDOW); + + return nullptr; +} + +LocalAccessible* DocAccessible::GetAccessibleByUniqueIDInSubtree( + void* aUniqueID) { + LocalAccessible* child = GetAccessibleByUniqueID(aUniqueID); + if (child) return child; + + uint32_t childDocCount = mChildDocuments.Length(); + for (uint32_t childDocIdx = 0; childDocIdx < childDocCount; childDocIdx++) { + DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx); + child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID); + if (child) return child; + } + + return nullptr; +} + +LocalAccessible* DocAccessible::GetAccessibleOrContainer( + nsINode* aNode, bool aNoContainerIfPruned) const { + if (!aNode || !aNode->GetComposedDoc()) { + return nullptr; + } + + nsINode* start = aNode; + if (auto* shadowRoot = dom::ShadowRoot::FromNode(aNode)) { + // This can happen, for example, when called within + // SelectionManager::ProcessSelectionChanged due to focusing a direct + // child of a shadow root. + // GetFlattenedTreeParent works on children of a shadow root, but not the + // shadow root itself. + start = shadowRoot->GetHost(); + if (!start) { + return nullptr; + } + } + + for (nsINode* currNode : dom::InclusiveFlatTreeAncestors(*start)) { + // No container if is inside of aria-hidden subtree. + if (aNoContainerIfPruned && currNode->IsElement() && + aria::HasDefinedARIAHidden(currNode->AsElement())) { + return nullptr; + } + + // Check if node is in zero-sized map + if (aNoContainerIfPruned && currNode->IsHTMLElement(nsGkAtoms::map)) { + if (nsIFrame* frame = currNode->AsContent()->GetPrimaryFrame()) { + if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame->GetParent()) + .IsEmpty()) { + return nullptr; + } + } + } + + if (LocalAccessible* accessible = GetAccessible(currNode)) { + return accessible; + } + } + + return nullptr; +} + +LocalAccessible* DocAccessible::GetContainerAccessible(nsINode* aNode) const { + return aNode ? GetAccessibleOrContainer(aNode->GetFlattenedTreeParentNode()) + : nullptr; +} + +LocalAccessible* DocAccessible::GetAccessibleOrDescendant( + nsINode* aNode) const { + LocalAccessible* acc = GetAccessible(aNode); + if (acc) return acc; + + if (aNode == mContent || aNode == mDocumentNode->GetRootElement()) { + // If the node is the doc's body or root element, return the doc accessible. + return const_cast<DocAccessible*>(this); + } + + acc = GetContainerAccessible(aNode); + if (acc) { + TreeWalker walker(acc, aNode->AsContent(), + TreeWalker::eWalkCache | TreeWalker::eScoped); + return walker.Next(); + } + + return nullptr; +} + +void DocAccessible::BindToDocument(LocalAccessible* aAccessible, + const nsRoleMapEntry* aRoleMapEntry) { + // Put into DOM node cache. + if (aAccessible->IsNodeMapEntry()) { + mNodeToAccessibleMap.InsertOrUpdate(aAccessible->GetNode(), aAccessible); + } + + // Put into unique ID cache. + mAccessibleCache.InsertOrUpdate(aAccessible->UniqueID(), RefPtr{aAccessible}); + + aAccessible->SetRoleMapEntry(aRoleMapEntry); + + if (aAccessible->HasOwnContent()) { + AddDependentIDsFor(aAccessible); + + nsIContent* content = aAccessible->GetContent(); + if (content->IsElement() && + content->AsElement()->HasAttr(nsGkAtoms::aria_owns)) { + mNotificationController->ScheduleRelocation(aAccessible); + } + } + + if (mIPCDoc) { + mInsertedAccessibles.EnsureInserted(aAccessible); + } + + QueueCacheUpdateForDependentRelations(aAccessible); +} + +void DocAccessible::UnbindFromDocument(LocalAccessible* aAccessible) { + NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()), + "Unbinding the unbound accessible!"); + + // Fire focus event on accessible having DOM focus if last focus was removed + // from the tree. + if (FocusMgr()->WasLastFocused(aAccessible)) { + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible); + } +#endif + } + + // Remove an accessible from node-to-accessible map if it exists there. + if (aAccessible->IsNodeMapEntry() && + mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible) { + mNodeToAccessibleMap.Remove(aAccessible->GetNode()); + } + + aAccessible->mStateFlags |= eIsNotInDocument; + + // Update XPCOM part. + xpcAccessibleDocument* xpcDoc = GetAccService()->GetCachedXPCDocument(this); + if (xpcDoc) xpcDoc->NotifyOfShutdown(aAccessible); + + void* uniqueID = aAccessible->UniqueID(); + + NS_ASSERTION(!aAccessible->IsDefunct(), "Shutdown the shutdown accessible!"); + aAccessible->Shutdown(); + + mAccessibleCache.Remove(uniqueID); +} + +void DocAccessible::ContentInserted(nsIContent* aStartChildNode, + nsIContent* aEndChildNode) { + // Ignore content insertions until we constructed accessible tree. Otherwise + // schedule tree update on content insertion after layout. + if (!mNotificationController || !HasLoadState(eTreeConstructed)) { + return; + } + + // The frame constructor guarantees that only ranges with the same parent + // arrive here in presence of dynamic changes to the page, see + // nsCSSFrameConstructor::IssueSingleInsertNotifications' callers. + nsINode* parent = aStartChildNode->GetFlattenedTreeParentNode(); + if (!parent) { + return; + } + + LocalAccessible* container = AccessibleOrTrueContainer(parent); + if (!container) { + return; + } + + AutoTArray<nsCOMPtr<nsIContent>, 10> list; + for (nsIContent* node = aStartChildNode; node != aEndChildNode; + node = node->GetNextSibling()) { + MOZ_ASSERT(parent == node->GetFlattenedTreeParentNode()); + if (PruneOrInsertSubtree(node)) { + list.AppendElement(node); + } + } + + mNotificationController->ScheduleContentInsertion(container, list); +} + +void DocAccessible::ScheduleTreeUpdate(nsIContent* aContent) { + if (mPendingUpdates.Contains(aContent)) { + return; + } + mPendingUpdates.AppendElement(aContent); + mNotificationController->ScheduleProcessing(); +} + +void DocAccessible::ProcessPendingUpdates() { + auto updates = std::move(mPendingUpdates); + for (auto update : updates) { + if (update->GetComposedDoc() != mDocumentNode) { + continue; + } + // The pruning logic will take care of avoiding unnecessary notifications. + ContentInserted(update, update->GetNextSibling()); + } +} + +bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) { + bool insert = false; + + // In the case that we are, or are in, a shadow host, we need to assure + // some accessibles are removed if they are not rendered anymore. + nsIContent* shadowHost = + aRoot->GetShadowRoot() ? aRoot : aRoot->GetContainingShadowHost(); + if (shadowHost) { + // Check all explicit children in the host, if they are not slotted + // then remove their accessibles and subtrees. + for (nsIContent* childNode = shadowHost->GetFirstChild(); childNode; + childNode = childNode->GetNextSibling()) { + if (!childNode->GetPrimaryFrame() && + !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) { + ContentRemoved(childNode); + } + } + + // If this is a slot, check to see if its fallback content is rendered, + // if not - remove it. + if (aRoot->IsHTMLElement(nsGkAtoms::slot)) { + for (nsIContent* childNode = aRoot->GetFirstChild(); childNode; + childNode = childNode->GetNextSibling()) { + if (!childNode->GetPrimaryFrame() && + !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) { + ContentRemoved(childNode); + } + } + } + } + + // If we already have an accessible, check if we need to remove it, recreate + // it, or keep it in place. + LocalAccessible* acc = GetAccessible(aRoot); + if (acc) { + MOZ_ASSERT(aRoot == acc->GetContent(), + "LocalAccessible has differing content!"); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin( + "TREE", "inserted content already has accessible; doc: %p", this); + logging::Node("content node", aRoot); + logging::AccessibleInfo("accessible node", acc); + logging::MsgEnd(); + } +#endif + + nsIFrame* frame = acc->GetFrame(); + if (frame) { + acc->MaybeQueueCacheUpdateForStyleChanges(); + } + + // LocalAccessible has no frame and it's not display:contents. Remove it. + // As well as removing the a11y subtree, we must also remove Accessibles + // for DOM descendants, since some of these might be relocated Accessibles + // and their DOM nodes are now hidden as well. + if (!frame && !nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) { + ContentRemoved(aRoot); + return false; + } + + // If it's a XULLabel it was probably reframed because a `value` attribute + // was added. The accessible creates its text leaf upon construction, so we + // need to recreate. Remove it, and schedule for reconstruction. + if (acc->IsXULLabel()) { + ContentRemoved(acc); + return true; + } + + // It is a broken image that is being reframed because it either got + // or lost an `alt` tag that would rerender this node as text. + if (frame && (acc->IsImage() != (frame->AccessibleType() == eImageType))) { + ContentRemoved(aRoot); + return true; + } + + // If the frame is an OuterDoc frame but this isn't an OuterDocAccessible, + // we need to recreate the LocalAccessible. This can happen for embed or + // object elements if their embedded content changes to be web content. + if (frame && !acc->IsOuterDoc() && + frame->AccessibleType() == eOuterDocType) { + ContentRemoved(aRoot); + return true; + } + + // If the content is focused, and is being re-framed, reset the selection + // listener for the node because the previous selection listener is on the + // old frame. + if (aRoot->IsElement() && FocusMgr()->HasDOMFocus(aRoot)) { + SelectionMgr()->SetControlSelectionListener(aRoot->AsElement()); + } + + // If the accessible is a table, or table part, its layout table + // status may have changed. We need to invalidate the associated + // mac table cache, which listens for the following event. We don't + // use this cache when the core cache is enabled, so to minimise event + // traffic only fire this event when that cache is off. + if (acc->IsTable() || acc->IsTableRow() || acc->IsTableCell()) { + LocalAccessible* table = nsAccUtils::TableFor(acc); + if (table && table->IsTable()) { + if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED, + table); + } + QueueCacheUpdate(table, CacheDomain::Table); + } + } + + // The accessible can be reparented or reordered in its parent. + // We schedule it for reinsertion. For example, a slotted element + // can change its slot attribute to a different slot. + insert = true; + + // If the frame is invisible, remove it. + // Normally, layout sends explicit a11y notifications for visibility + // changes (see SendA11yNotifications in RestyleManager). However, if a + // visibility change also reconstructs the frame, we must handle it here. + if (frame && !frame->StyleVisibility()->IsVisible()) { + ContentRemoved(aRoot); + // There might be visible descendants, so we want to walk the subtree. + // However, we know we don't want to reinsert this node, so we set insert + // to false. + insert = false; + } + } else { + // If there is no current accessible, and the node has a frame, or is + // display:contents, schedule it for insertion. + if (aRoot->GetPrimaryFrame() || + nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) { + // This may be a new subtree, the insertion process will recurse through + // its descendants. + if (!GetAccessibleOrDescendant(aRoot)) { + return true; + } + + // Content is not an accessible, but has accessible descendants. + // We schedule this container for insertion strictly for the case where it + // itself now needs an accessible. We will still need to recurse into the + // descendant content to prune accessibles, and in all likelyness to + // insert accessibles since accessible insertions will likeley get missed + // in an existing subtree. + insert = true; + } + } + + if (LocalAccessible* container = AccessibleOrTrueContainer(aRoot)) { + AutoTArray<nsCOMPtr<nsIContent>, 10> list; + dom::AllChildrenIterator iter = + dom::AllChildrenIterator(aRoot, nsIContent::eAllChildren, true); + while (nsIContent* childNode = iter.GetNextChild()) { + if (PruneOrInsertSubtree(childNode)) { + list.AppendElement(childNode); + } + } + + if (!list.IsEmpty()) { + mNotificationController->ScheduleContentInsertion(container, list); + } + } + + return insert; +} + +void DocAccessible::RecreateAccessible(nsIContent* aContent) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "accessible recreated"); + logging::Node("content", aContent); + logging::MsgEnd(); + } +#endif + + // XXX: we shouldn't recreate whole accessible subtree, instead we should + // subclass hide and show events to handle them separately and implement their + // coalescence with normal hide and show events. Note, in this case they + // should be coalesced with normal show/hide events. + ContentRemoved(aContent); + ContentInserted(aContent, aContent->GetNextSibling()); +} + +void DocAccessible::ProcessInvalidationList() { + // Invalidate children of container accessible for each element in + // invalidation list. Allow invalidation list insertions while container + // children are recached. + for (uint32_t idx = 0; idx < mInvalidationList.Length(); idx++) { + nsIContent* content = mInvalidationList[idx]; + if (!HasAccessible(content) && content->HasID()) { + LocalAccessible* container = GetContainerAccessible(content); + if (container) { + // Check if the node is a target of aria-owns, and if so, don't process + // it here and let DoARIAOwnsRelocation process it. + AttrRelProviders* list = GetRelProviders( + content->AsElement(), nsDependentAtomString(content->GetID())); + bool shouldProcess = !!list; + if (shouldProcess) { + for (uint32_t idx = 0; idx < list->Length(); idx++) { + if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) { + shouldProcess = false; + break; + } + } + + if (shouldProcess) { + ProcessContentInserted(container, content); + } + } + } + } + } + + mInvalidationList.Clear(); +} + +void DocAccessible::ProcessQueuedCacheUpdates() { + if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) { + return; + } + + AUTO_PROFILER_MARKER_TEXT("DocAccessible::ProcessQueuedCacheUpdates", A11Y, + {}, ""_ns); + // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. + + nsTArray<CacheData> data; + for (auto iter = mQueuedCacheUpdates.Iter(); !iter.Done(); iter.Next()) { + LocalAccessible* acc = iter.Key(); + uint64_t domain = iter.UserData(); + if (acc && acc->IsInDocument() && !acc->IsDefunct()) { + RefPtr<AccAttributes> fields = + acc->BundleFieldsForCache(domain, CacheUpdateType::Update); + + if (fields->Count()) { + data.AppendElement(CacheData( + acc->IsDoc() ? 0 : reinterpret_cast<uint64_t>(acc->UniqueID()), + fields)); + } + } + } + + mQueuedCacheUpdates.Clear(); + + if (mViewportCacheDirty) { + RefPtr<AccAttributes> fields = + BundleFieldsForCache(CacheDomain::Viewport, CacheUpdateType::Update); + if (fields->Count()) { + data.AppendElement(CacheData(0, fields)); + } + mViewportCacheDirty = false; + } + + if (data.Length()) { + IPCDoc()->SendCache(CacheUpdateType::Update, data, false); + } +} + +void DocAccessible::SendAccessiblesWillMove() { + if (!mIPCDoc) { + return; + } + nsTArray<uint64_t> ids; + for (LocalAccessible* acc : mMovedAccessibles) { + // If acc is defunct or not in a document, it was removed after it was + // moved. + if (!acc->IsDefunct() && acc->IsInDocument()) { + ids.AppendElement(reinterpret_cast<uintptr_t>(acc->UniqueID())); + } + } + if (!ids.IsEmpty()) { + mIPCDoc->SendAccessiblesWillMove(ids); + } +} + +LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMap( + nsINode* aNode) const { + if (!aNode->IsContent() || + !aNode->AsContent()->IsHTMLElement(nsGkAtoms::area)) { + return GetAccessible(aNode); + } + + // XXX Bug 135040, incorrect when multiple images use the same map. + nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame(); + nsImageFrame* imageFrame = do_QueryFrame(frame); + if (imageFrame) { + LocalAccessible* parent = GetAccessible(imageFrame->GetContent()); + if (parent) { + LocalAccessible* area = + parent->AsImageMap()->GetChildAccessibleFor(aNode); + if (area) return area; + + return nullptr; + } + } + + return GetAccessible(aNode); +} + +//////////////////////////////////////////////////////////////////////////////// +// Protected members + +void DocAccessible::NotifyOfLoading(bool aIsReloading) { + // Mark the document accessible as loading, if it stays alive then we'll mark + // it as loaded when we receive proper notification. + mLoadState &= ~eDOMLoaded; + + if (!IsLoadEventTarget()) return; + + if (aIsReloading && !mLoadEventType && + // We can't fire events on a document whose tree isn't constructed yet. + HasLoadState(eTreeConstructed)) { + // Fire reload and state busy events on existing document accessible while + // event from user input flag can be calculated properly and accessible + // is alive. When new document gets loaded then this one is destroyed. + RefPtr<AccEvent> reloadEvent = + new AccEvent(nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD, this); + nsEventShell::FireEvent(reloadEvent); + } + + // Fire state busy change event. Use delayed event since we don't care + // actually if event isn't delivered when the document goes away like a shot. + RefPtr<AccEvent> stateEvent = + new AccStateChangeEvent(this, states::BUSY, true); + FireDelayedEvent(stateEvent); +} + +void DocAccessible::DoInitialUpdate() { + AUTO_PROFILER_MARKER_TEXT("DocAccessible::DoInitialUpdate", A11Y, {}, ""_ns); + // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. + + if (nsCoreUtils::IsTopLevelContentDocInProcess(mDocumentNode)) { + mDocFlags |= eTopLevelContentDocInProcess; + if (IPCAccessibilityActive()) { + nsIDocShell* docShell = mDocumentNode->GetDocShell(); + if (RefPtr<dom::BrowserChild> browserChild = + dom::BrowserChild::GetFrom(docShell)) { + // In content processes, top level content documents are always + // RootAccessibles. + MOZ_ASSERT(IsRoot()); + DocAccessibleChild* ipcDoc = IPCDoc(); + if (ipcDoc) { + browserChild->SetTopLevelDocAccessibleChild(ipcDoc); + } else { + ipcDoc = new DocAccessibleChild(this, browserChild); + SetIPCDoc(ipcDoc); + // Subsequent initialization might depend on being able to get the + // top level DocAccessibleChild, so set that as early as possible. + browserChild->SetTopLevelDocAccessibleChild(ipcDoc); + +#if defined(XP_WIN) + IAccessibleHolder holder; + int32_t childID; + if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { + childID = 0; + } else { + holder = CreateHolderFromAccessible(WrapNotNull(this)); + MOZ_ASSERT(!holder.IsNull()); + childID = MsaaAccessible::GetChildIDFor(this); + } +#else + int32_t holder = 0, childID = 0; +#endif + browserChild->SendPDocAccessibleConstructor( + ipcDoc, nullptr, 0, mDocumentNode->GetBrowsingContext(), childID, + holder); +#if !defined(XP_WIN) + ipcDoc->SendPDocAccessiblePlatformExtConstructor(); +#endif + } +#if !defined(XP_WIN) + // It's safe for us to mark top level documents as constructed in the + // parent process without receiving an explicit message, since we can + // never get queries for this document or descendants before parent + // process construction is complete. + ipcDoc->SetConstructedInParentProcess(); +#endif + } + } + } + + mLoadState |= eTreeConstructed; + + // Set up a root element and ARIA role mapping. + UpdateRootElIfNeeded(); + + // Build initial tree. + CacheChildrenInSubtree(this); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eVerbose)) { + logging::Tree("TREE", "Initial subtree", this); + } + if (logging::IsEnabled(logging::eTreeSize)) { + logging::TreeSize("TREE SIZE", "Initial subtree", this); + } +#endif + + // Fire reorder event after the document tree is constructed. Note, since + // this reorder event is processed by parent document then events targeted to + // this document may be fired prior to this reorder event. If this is + // a problem then consider to keep event processing per tab document. + if (!IsRoot()) { + RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(LocalParent()); + ParentDocument()->FireDelayedEvent(reorderEvent); + } + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + if (IPCAccessibilityActive()) { + DocAccessibleChild* ipcDoc = IPCDoc(); + MOZ_ASSERT(ipcDoc); + if (ipcDoc) { + if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // If we're caching, we should send an initial update for this document + // and its attributes. Each acc contained in this doc will have its + // initial update sent in `InsertIntoIpcTree`. + SendCache(CacheDomain::All, CacheUpdateType::Initial); + } + + for (auto idx = 0U; idx < mChildren.Length(); idx++) { + ipcDoc->InsertIntoIpcTree(this, mChildren.ElementAt(idx), idx, true); + } + } + } +} + +void DocAccessible::ProcessLoad() { + mLoadState |= eCompletelyLoaded; + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) { + logging::DocCompleteLoad(this, IsLoadEventTarget()); + } +#endif + + // Do not fire document complete/stop events for root chrome document + // accessibles and for frame/iframe documents because + // a) screen readers start working on focus event in the case of root chrome + // documents + // b) document load event on sub documents causes screen readers to act is if + // entire page is reloaded. + if (!IsLoadEventTarget()) return; + + // Fire complete/load stopped if the load event type is given. + if (mLoadEventType) { + RefPtr<AccEvent> loadEvent = new AccEvent(mLoadEventType, this); + FireDelayedEvent(loadEvent); + + mLoadEventType = 0; + } + + // Fire busy state change event. + RefPtr<AccEvent> stateEvent = + new AccStateChangeEvent(this, states::BUSY, false); + FireDelayedEvent(stateEvent); +} + +void DocAccessible::AddDependentIDsFor(LocalAccessible* aRelProvider, + nsAtom* aRelAttr) { + dom::Element* relProviderEl = aRelProvider->Elm(); + if (!relProviderEl) return; + + for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) { + nsStaticAtom* relAttr = kRelationAttrs[idx]; + if (aRelAttr && aRelAttr != relAttr) continue; + + if (relAttr == nsGkAtoms::_for) { + if (!relProviderEl->IsAnyOfHTMLElements(nsGkAtoms::label, + nsGkAtoms::output)) { + continue; + } + + } else if (relAttr == nsGkAtoms::control) { + if (!relProviderEl->IsAnyOfXULElements(nsGkAtoms::label, + nsGkAtoms::description)) { + continue; + } + } + + IDRefsIterator iter(this, relProviderEl, relAttr); + while (true) { + const nsDependentSubstring id = iter.NextID(); + if (id.IsEmpty()) break; + + AttrRelProviders* providers = GetOrCreateRelProviders(relProviderEl, id); + if (providers) { + AttrRelProvider* provider = new AttrRelProvider(relAttr, relProviderEl); + if (provider) { + providers->AppendElement(provider); + + // We've got here during the children caching. If the referenced + // content is not accessible then store it to pend its container + // children invalidation (this happens immediately after the caching + // is finished). + nsIContent* dependentContent = iter.GetElem(id); + if (dependentContent) { + if (!HasAccessible(dependentContent)) { + mInvalidationList.AppendElement(dependentContent); + } + } + } + } + } + + // If the relation attribute is given then we don't have anything else to + // check. + if (aRelAttr) break; + } + + // Make sure to schedule the tree update if needed. + mNotificationController->ScheduleProcessing(); +} + +void DocAccessible::RemoveDependentIDsFor(LocalAccessible* aRelProvider, + nsAtom* aRelAttr) { + dom::Element* relProviderElm = aRelProvider->Elm(); + if (!relProviderElm) return; + + for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) { + nsStaticAtom* relAttr = kRelationAttrs[idx]; + if (aRelAttr && aRelAttr != kRelationAttrs[idx]) continue; + + IDRefsIterator iter(this, relProviderElm, relAttr); + while (true) { + const nsDependentSubstring id = iter.NextID(); + if (id.IsEmpty()) break; + + AttrRelProviders* providers = GetRelProviders(relProviderElm, id); + if (providers) { + providers->RemoveElementsBy( + [relAttr, relProviderElm](const auto& provider) { + return provider->mRelAttr == relAttr && + provider->mContent == relProviderElm; + }); + + RemoveRelProvidersIfEmpty(relProviderElm, id); + } + } + + // If the relation attribute is given then we don't have anything else to + // check. + if (aRelAttr) break; + } +} + +bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement, + nsAtom* aAttribute) { + if (aAttribute == nsGkAtoms::role) { + // It is common for js libraries to set the role on the body element after + // the document has loaded. In this case we just update the role map entry. + if (mContent == aElement) { + SetRoleMapEntryForDoc(aElement); + if (mIPCDoc) { + mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex); + } + + return true; + } + + // Recreate the accessible when role is changed because we might require a + // different accessible class for the new role or the accessible may expose + // a different sets of interfaces (COM restriction). + RecreateAccessible(aElement); + + return true; + } + + if (aAttribute == nsGkAtoms::multiple) { + if (dom::HTMLSelectElement* select = + dom::HTMLSelectElement::FromNode(aElement)) { + if (select->Size() <= 1) { + // Adding the 'multiple' attribute to a select that has a size of 1 + // creates a listbox as opposed to a combobox with a popup combobox + // list. Removing the attribute does the opposite. + RecreateAccessible(aElement); + return true; + } + } + } + + if (aAttribute == nsGkAtoms::size && + aElement->IsHTMLElement(nsGkAtoms::select)) { + // Changing the size of a select element can potentially change it from a + // combobox button to a listbox with different underlying implementations. + RecreateAccessible(aElement); + return true; + } + + if (aAttribute == nsGkAtoms::type) { + // If the input[type] changes, we should recreate the accessible. + RecreateAccessible(aElement); + return true; + } + + if (aElement->IsHTMLElement(nsGkAtoms::img) && aAttribute == nsGkAtoms::alt) { + // If alt text changes on an img element, we may want to create or remove an + // accessible for that img. + if (nsAccessibilityService::ShouldCreateImgAccessible(aElement, this)) { + if (GetAccessible(aElement)) { + // If the accessible already exists, there's no need to create one. + return false; + } + ContentInserted(aElement, aElement->GetNextSibling()); + } else { + ContentRemoved(aElement); + } + return true; + } + + return false; +} + +void DocAccessible::UpdateRootElIfNeeded() { + dom::Element* rootEl = mDocumentNode->GetBodyElement(); + if (!rootEl) { + rootEl = mDocumentNode->GetRootElement(); + } + if (rootEl != mContent) { + mContent = rootEl; + SetRoleMapEntryForDoc(rootEl); + if (mIPCDoc) { + mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex); + } + } +} + +/** + * Content insertion helper. + */ +class InsertIterator final { + public: + InsertIterator(LocalAccessible* aContext, + const nsTArray<nsCOMPtr<nsIContent>>* aNodes) + : mChild(nullptr), + mChildBefore(nullptr), + mWalker(aContext), + mNodes(aNodes), + mNodesIdx(0) { + MOZ_ASSERT(aContext, "No context"); + MOZ_ASSERT(aNodes, "No nodes to search for accessible elements"); + MOZ_COUNT_CTOR(InsertIterator); + } + MOZ_COUNTED_DTOR(InsertIterator) + + LocalAccessible* Context() const { return mWalker.Context(); } + LocalAccessible* Child() const { return mChild; } + LocalAccessible* ChildBefore() const { return mChildBefore; } + DocAccessible* Document() const { return mWalker.Document(); } + + /** + * Iterates to a next accessible within the inserted content. + */ + bool Next(); + + void Rejected() { + mChild = nullptr; + mChildBefore = nullptr; + } + + private: + LocalAccessible* mChild; + LocalAccessible* mChildBefore; + TreeWalker mWalker; + + const nsTArray<nsCOMPtr<nsIContent>>* mNodes; + nsTHashSet<nsPtrHashKey<const nsIContent>> mProcessedNodes; + uint32_t mNodesIdx; +}; + +bool InsertIterator::Next() { + if (mNodesIdx > 0) { + // If we already processed the first node in the mNodes list, + // check if we can just use the walker to get its next sibling. + LocalAccessible* nextChild = mWalker.Next(); + if (nextChild) { + mChildBefore = mChild; + mChild = nextChild; + return true; + } + } + + while (mNodesIdx < mNodes->Length()) { + nsIContent* node = mNodes->ElementAt(mNodesIdx++); + // Check to see if we already processed this node with this iterator. + // this can happen if we get two redundant insertions in the case of a + // text and frame insertion. + if (!mProcessedNodes.EnsureInserted(node)) { + continue; + } + + LocalAccessible* container = Document()->AccessibleOrTrueContainer( + node->GetFlattenedTreeParentNode(), true); + // Ignore nodes that are not contained by the container anymore. + // The container might be changed, for example, because of the subsequent + // overlapping content insertion (i.e. other content was inserted between + // this inserted content and its container or the content was reinserted + // into different container of unrelated part of tree). To avoid a double + // processing of the content insertion ignore this insertion notification. + // Note, the inserted content might be not in tree at all at this point + // what means there's no container. Ignore the insertion too. + if (container != Context()) { + continue; + } + + // HTML comboboxes have no-content list accessible as an intermediate + // containing all options. + if (container->IsHTMLCombobox()) { + container = container->LocalFirstChild(); + } + + if (!container->IsAcceptableChild(node)) { + continue; + } + +#ifdef A11Y_LOG + logging::TreeInfo("traversing an inserted node", logging::eVerbose, + "container", container, "node", node); +#endif + + nsIContent* prevNode = mChild ? mChild->GetContent() : nullptr; + if (prevNode && prevNode->GetNextSibling() == node) { + // If inserted nodes are siblings then just move the walker next. + LocalAccessible* nextChild = mWalker.Scope(node); + if (nextChild) { + mChildBefore = mChild; + mChild = nextChild; + return true; + } + } else { + // Otherwise use a new walker to find this node in the container's + // subtree, and retrieve its preceding sibling. + TreeWalker finder(container); + if (finder.Seek(node)) { + mChild = mWalker.Scope(node); + if (mChild) { + MOZ_ASSERT(!mChild->IsRelocated(), "child cannot be aria owned"); + mChildBefore = finder.Prev(); + return true; + } + } + } + } + + return false; +} + +void DocAccessible::ProcessContentInserted( + LocalAccessible* aContainer, const nsTArray<nsCOMPtr<nsIContent>>* aNodes) { + // Process insertions if the container accessible is still in tree. + if (!aContainer->IsInDocument()) { + return; + } + + // If new root content has been inserted then update it. + if (aContainer == this) { + UpdateRootElIfNeeded(); + } + + InsertIterator iter(aContainer, aNodes); + if (!iter.Next()) { + return; + } + +#ifdef A11Y_LOG + logging::TreeInfo("children before insertion", logging::eVerbose, aContainer); +#endif + + TreeMutation mt(aContainer); + do { + LocalAccessible* parent = iter.Child()->LocalParent(); + if (parent) { + LocalAccessible* previousSibling = iter.ChildBefore(); + if (parent != aContainer || + iter.Child()->LocalPrevSibling() != previousSibling) { + if (previousSibling && previousSibling->LocalParent() != aContainer) { + // previousSibling hasn't been moved into aContainer yet. + // previousSibling should be later in the insertion list, so the tree + // will get adjusted when we process it later. + MOZ_DIAGNOSTIC_ASSERT(parent == aContainer, + "Child moving to new parent, but previous " + "sibling in wrong parent"); + continue; + } +#ifdef A11Y_LOG + logging::TreeInfo("relocating accessible", 0, "old parent", parent, + "new parent", aContainer, "child", iter.Child(), + nullptr); +#endif + MoveChild(iter.Child(), aContainer, + previousSibling ? previousSibling->IndexInParent() + 1 : 0); + } + continue; + } + + if (aContainer->InsertAfter(iter.Child(), iter.ChildBefore())) { +#ifdef A11Y_LOG + logging::TreeInfo("accessible was inserted", 0, "container", aContainer, + "child", iter.Child(), nullptr); +#endif + + CreateSubtree(iter.Child()); + mt.AfterInsertion(iter.Child()); + continue; + } + + MOZ_ASSERT_UNREACHABLE("accessible was rejected"); + iter.Rejected(); + } while (iter.Next()); + + mt.Done(); + +#ifdef A11Y_LOG + logging::TreeInfo("children after insertion", logging::eVerbose, aContainer); +#endif + + FireEventsOnInsertion(aContainer); +} + +void DocAccessible::ProcessContentInserted(LocalAccessible* aContainer, + nsIContent* aNode) { + if (!aContainer->IsInDocument()) { + return; + } + +#ifdef A11Y_LOG + logging::TreeInfo("children before insertion", logging::eVerbose, aContainer); +#endif + +#ifdef A11Y_LOG + logging::TreeInfo("traversing an inserted node", logging::eVerbose, + "container", aContainer, "node", aNode); +#endif + + TreeWalker walker(aContainer); + if (aContainer->IsAcceptableChild(aNode) && walker.Seek(aNode)) { + LocalAccessible* child = GetAccessible(aNode); + if (!child) { + child = GetAccService()->CreateAccessible(aNode, aContainer); + } + + if (child) { + TreeMutation mt(aContainer); + if (!aContainer->InsertAfter(child, walker.Prev())) { + return; + } + CreateSubtree(child); + mt.AfterInsertion(child); + mt.Done(); + + FireEventsOnInsertion(aContainer); + } + } + +#ifdef A11Y_LOG + logging::TreeInfo("children after insertion", logging::eVerbose, aContainer); +#endif +} + +void DocAccessible::FireEventsOnInsertion(LocalAccessible* aContainer) { + // Check to see if change occurred inside an alert, and fire an EVENT_ALERT + // if it did. + if (aContainer->IsAlert() || aContainer->IsInsideAlert()) { + LocalAccessible* ancestor = aContainer; + do { + if (ancestor->IsAlert()) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor); + break; + } + } while ((ancestor = ancestor->LocalParent())); + } +} + +void DocAccessible::ContentRemoved(LocalAccessible* aChild) { + MOZ_DIAGNOSTIC_ASSERT(aChild != this, "Should never be called for the doc"); + LocalAccessible* parent = aChild->LocalParent(); + MOZ_DIAGNOSTIC_ASSERT(parent, "Unattached accessible from tree"); + +#ifdef A11Y_LOG + logging::TreeInfo("process content removal", 0, "container", parent, "child", + aChild, nullptr); +#endif + + // XXX: event coalescence may kill us + RefPtr<LocalAccessible> kungFuDeathGripChild(aChild); + + TreeMutation mt(parent); + mt.BeforeRemoval(aChild); + + if (aChild->IsDefunct()) { + MOZ_ASSERT_UNREACHABLE("Event coalescence killed the accessible"); + mt.Done(); + return; + } + + MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Alive but unparented #1"); + + if (aChild->IsRelocated()) { + nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(parent); + MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash"); + owned->RemoveElement(aChild); + if (owned->Length() == 0) { + mARIAOwnsHash.Remove(parent); + } + } + MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Unparented #2"); + UncacheChildrenInSubtree(aChild); + parent->RemoveChild(aChild); + + mt.Done(); +} + +void DocAccessible::ContentRemoved(nsIContent* aContentNode) { + // If child node is not accessible then look for its accessible children. + LocalAccessible* acc = GetAccessible(aContentNode); + if (acc) { + ContentRemoved(acc); + } + + dom::AllChildrenIterator iter = + dom::AllChildrenIterator(aContentNode, nsIContent::eAllChildren, true); + while (nsIContent* childNode = iter.GetNextChild()) { + ContentRemoved(childNode); + } + + // If this node has a shadow root, remove its explicit children too. + // The host node may be removed after the shadow root was attached, and + // before we asynchronously prune the light DOM and construct the shadow DOM. + // If this is a case where the node does not have its own accessible, we will + // not recurse into its current children, so we need to use an + // ExplicitChildIterator in order to get its accessible children in the light + // DOM, since they are not accessible anymore via AllChildrenIterator. + if (aContentNode->GetShadowRoot()) { + for (nsIContent* childNode = aContentNode->GetFirstChild(); childNode; + childNode = childNode->GetNextSibling()) { + ContentRemoved(childNode); + } + } +} + +bool DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) { + if (!aElement->HasID()) return false; + + AttrRelProviders* list = GetRelProviders( + aElement->AsElement(), nsDependentAtomString(aElement->GetID())); + if (list) { + for (uint32_t idx = 0; idx < list->Length(); idx++) { + if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) { + LocalAccessible* owner = GetAccessible(list->ElementAt(idx)->mContent); + if (owner) { + mNotificationController->ScheduleRelocation(owner); + return true; + } + } + } + } + + return false; +} + +void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) { + MOZ_ASSERT(aOwner, "aOwner must be a valid pointer"); + MOZ_ASSERT(aOwner->Elm(), "aOwner->Elm() must be a valid pointer"); + +#ifdef A11Y_LOG + logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner); +#endif + + nsTArray<RefPtr<LocalAccessible>>* owned = + mARIAOwnsHash.GetOrInsertNew(aOwner); + + IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns); + uint32_t idx = 0; + while (nsIContent* childEl = iter.NextElem()) { + LocalAccessible* child = GetAccessible(childEl); + auto insertIdx = aOwner->ChildCount() - owned->Length() + idx; + + // Make an attempt to create an accessible if it wasn't created yet. + if (!child) { + // An owned child cannot be an ancestor of the owner. + bool ok = true; + bool check = true; + for (LocalAccessible* parent = aOwner; parent && !parent->IsDoc(); + parent = parent->LocalParent()) { + if (check) { + if (parent->Elm()->IsInclusiveDescendantOf(childEl)) { + ok = false; + break; + } + } + // We need to do the DOM descendant check again whenever the DOM + // lineage changes. If parent is relocated, that means the next + // ancestor will have a different DOM lineage. + check = parent->IsRelocated(); + } + if (!ok) { + continue; + } + + if (aOwner->IsAcceptableChild(childEl)) { + child = GetAccService()->CreateAccessible(childEl, aOwner); + if (child) { + TreeMutation imut(aOwner); + aOwner->InsertChildAt(insertIdx, child); + imut.AfterInsertion(child); + imut.Done(); + + child->SetRelocated(true); + owned->InsertElementAt(idx, child); + idx++; + + // Create subtree before adjusting the insertion index, since subtree + // creation may alter children in the container. + CreateSubtree(child); + FireEventsOnInsertion(aOwner); + } + } + continue; + } + +#ifdef A11Y_LOG + logging::TreeInfo("aria owns traversal", logging::eVerbose, "candidate", + child, nullptr); +#endif + + if (owned->IndexOf(child) < idx) { + continue; // ignore second entry of same ID + } + + // Same child on same position, no change. + if (child->LocalParent() == aOwner) { + int32_t indexInParent = child->IndexInParent(); + + // The child is being placed in its current index, + // eg. aria-owns='id1 id2 id3' is changed to aria-owns='id3 id2 id1'. + if (indexInParent == static_cast<int32_t>(insertIdx)) { + MOZ_ASSERT(child->IsRelocated(), + "A child, having an index in parent from aria ownded " + "indices range, has to be aria owned"); + MOZ_ASSERT(owned->ElementAt(idx) == child, + "Unexpected child in ARIA owned array"); + idx++; + continue; + } + + // The child is being inserted directly after its current index, + // resulting in a no-move case. This will happen when a parent aria-owns + // its last ordinal child: + // <ul aria-owns='id2'><li id='id1'></li><li id='id2'></li></ul> + if (indexInParent == static_cast<int32_t>(insertIdx) - 1) { + MOZ_ASSERT(!child->IsRelocated(), + "Child should be in its ordinal position"); + child->SetRelocated(true); + owned->InsertElementAt(idx, child); + idx++; + continue; + } + } + + MOZ_ASSERT(owned->SafeElementAt(idx) != child, "Already in place!"); + + // A new child is found, check for loops. + if (child->LocalParent() != aOwner) { + // Child is aria-owned by another container, skip. + if (child->IsRelocated()) { + continue; + } + + LocalAccessible* parent = aOwner; + while (parent && parent != child && !parent->IsDoc()) { + parent = parent->LocalParent(); + } + // A referred child cannot be a parent of the owner. + if (parent == child) { + continue; + } + } + + if (MoveChild(child, aOwner, insertIdx)) { + child->SetRelocated(true); + MOZ_ASSERT(owned == mARIAOwnsHash.Get(aOwner)); + owned = mARIAOwnsHash.GetOrInsertNew(aOwner); + owned->InsertElementAt(idx, child); + idx++; + } + } + + // Put back children that are not seized anymore. + PutChildrenBack(owned, idx); + if (owned->Length() == 0) { + mARIAOwnsHash.Remove(aOwner); + } +} + +void DocAccessible::PutChildrenBack( + nsTArray<RefPtr<LocalAccessible>>* aChildren, uint32_t aStartIdx) { + MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index"); + + for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) { + LocalAccessible* child = aChildren->ElementAt(idx); + if (!child->IsInDocument()) { + continue; + } + + // Remove the child from the owner + LocalAccessible* owner = child->LocalParent(); + if (!owner) { + NS_ERROR("Cannot put the child back. No parent, a broken tree."); + continue; + } + +#ifdef A11Y_LOG + logging::TreeInfo("aria owns put child back", 0, "old parent", owner, + "child", child, nullptr); +#endif + + // Unset relocated flag to find an insertion point for the child. + child->SetRelocated(false); + + nsIContent* content = child->GetContent(); + int32_t idxInParent = -1; + LocalAccessible* origContainer = + AccessibleOrTrueContainer(content->GetFlattenedTreeParentNode()); + if (origContainer) { + TreeWalker walker(origContainer); + if (walker.Seek(content)) { + LocalAccessible* prevChild = walker.Prev(); + if (prevChild) { + idxInParent = prevChild->IndexInParent() + 1; + MOZ_DIAGNOSTIC_ASSERT(origContainer == prevChild->LocalParent(), + "Broken tree"); + origContainer = prevChild->LocalParent(); + } else { + idxInParent = 0; + } + } + } + + // The child may have already be in its ordinal place for 2 reasons: + // 1. It was the last ordinal child, and the first aria-owned child. + // given: <ul id="list" aria-owns="b"><li id="a"></li><li + // id="b"></li></ul> after load: $("list").setAttribute("aria-owns", ""); + // 2. The preceding adopted children were just reclaimed, eg: + // given: <ul id="list"><li id="b"></li></ul> + // after load: $("list").setAttribute("aria-owns", "a b"); + // later: $("list").setAttribute("aria-owns", ""); + if (origContainer != owner || child->IndexInParent() != idxInParent) { + DebugOnly<bool> moved = MoveChild(child, origContainer, idxInParent); + MOZ_ASSERT(moved, "Failed to put child back."); + } else { + MOZ_ASSERT(!child->LocalPrevSibling() || + !child->LocalPrevSibling()->IsRelocated(), + "No relocated child should appear before this one"); + MOZ_ASSERT(!child->LocalNextSibling() || + child->LocalNextSibling()->IsRelocated(), + "No ordinal child should appear after this one"); + } + } + + aChildren->RemoveLastElements(aChildren->Length() - aStartIdx); +} + +void DocAccessible::TrackMovedAccessible(LocalAccessible* aAcc) { + // If an Accessible is inserted and moved during the same tick, don't track + // it as a move because it hasn't been shown yet. + if (!mInsertedAccessibles.Contains(aAcc)) { + mMovedAccessibles.EnsureInserted(aAcc); + } + // When we move an Accessible, we're also moving its descendants. + for (uint32_t c = 0, count = aAcc->ContentChildCount(); c < count; ++c) { + TrackMovedAccessible(aAcc->ContentChildAt(c)); + } +} + +bool DocAccessible::MoveChild(LocalAccessible* aChild, + LocalAccessible* aNewParent, + int32_t aIdxInParent) { + MOZ_ASSERT(aChild, "No child"); + MOZ_ASSERT(aChild->LocalParent(), "No parent"); + // We can't guarantee MoveChild works correctly for accessibilities storing + // children outside mChildren. + MOZ_ASSERT( + aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()), + "Wrong insertion point for a moving child"); + + LocalAccessible* curParent = aChild->LocalParent(); + + if (!aNewParent->IsAcceptableChild(aChild->GetContent())) { + return false; + } + +#ifdef A11Y_LOG + logging::TreeInfo("move child", 0, "old parent", curParent, "new parent", + aNewParent, "child", aChild, nullptr); +#endif + + // Forget aria-owns info in case of ARIA owned element. The caller is expected + // to update it if needed. + if (aChild->IsRelocated()) { + aChild->SetRelocated(false); + nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(curParent); + MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash"); + owned->RemoveElement(aChild); + if (owned->Length() == 0) { + mARIAOwnsHash.Remove(curParent); + } + } + + if (curParent == aNewParent) { + MOZ_ASSERT(aChild->IndexInParent() != aIdxInParent, "No move case"); + curParent->RelocateChild(aIdxInParent, aChild); + if (mIPCDoc) { + TrackMovedAccessible(aChild); + } + +#ifdef A11Y_LOG + logging::TreeInfo("move child: parent tree after", logging::eVerbose, + curParent); +#endif + return true; + } + + // If the child cannot be re-inserted into the tree, then make sure to remove + // it from its present parent and then shutdown it. + bool hasInsertionPoint = + (aIdxInParent >= 0) && + (aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length())); + + TreeMutation rmut(curParent); + rmut.BeforeRemoval(aChild, hasInsertionPoint && TreeMutation::kNoShutdown); + curParent->RemoveChild(aChild); + rmut.Done(); + + // No insertion point for the child. + if (!hasInsertionPoint) { + return true; + } + + TreeMutation imut(aNewParent); + aNewParent->InsertChildAt(aIdxInParent, aChild); + if (mIPCDoc) { + TrackMovedAccessible(aChild); + } + imut.AfterInsertion(aChild); + imut.Done(); + +#ifdef A11Y_LOG + logging::TreeInfo("move child: old parent tree after", logging::eVerbose, + curParent); + logging::TreeInfo("move child: new parent tree after", logging::eVerbose, + aNewParent); +#endif + + return true; +} + +void DocAccessible::CacheChildrenInSubtree(LocalAccessible* aRoot, + LocalAccessible** aFocusedAcc) { + // If the accessible is focused then report a focus event after all related + // mutation events. + if (aFocusedAcc && !*aFocusedAcc && + FocusMgr()->HasDOMFocus(aRoot->GetContent())) { + *aFocusedAcc = aRoot; + } + + LocalAccessible* root = + aRoot->IsHTMLCombobox() ? aRoot->LocalFirstChild() : aRoot; + if (root->KidsFromDOM()) { + TreeMutation mt(root, TreeMutation::kNoEvents); + TreeWalker walker(root); + while (LocalAccessible* child = walker.Next()) { + if (child->IsBoundToParent()) { + MoveChild(child, root, root->mChildren.Length()); + continue; + } + + root->AppendChild(child); + mt.AfterInsertion(child); + + CacheChildrenInSubtree(child, aFocusedAcc); + } + mt.Done(); + } + + // Fire events for ARIA elements. + if (!aRoot->HasARIARole()) { + return; + } + + // XXX: we should delay document load complete event if the ARIA document + // has aria-busy. + roles::Role role = aRoot->ARIARole(); + if (!aRoot->IsDoc() && + (role == roles::DIALOG || role == roles::NON_NATIVE_DOCUMENT)) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot); + } +} + +void DocAccessible::UncacheChildrenInSubtree(LocalAccessible* aRoot) { + aRoot->mStateFlags |= eIsNotInDocument; + RemoveDependentIDsFor(aRoot); + + // The parent of the removed subtree is about to be cleared, so we must do + // this here rather than in LocalAccessible::UnbindFromParent because we need + // the ancestry for this to work. + if (StaticPrefs::accessibility_cache_enabled_AtStartup() && + (aRoot->IsTable() || aRoot->IsTableCell())) { + CachedTableAccessible::Invalidate(aRoot); + } + + nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(aRoot); + uint32_t count = aRoot->ContentChildCount(); + for (uint32_t idx = 0; idx < count; idx++) { + LocalAccessible* child = aRoot->ContentChildAt(idx); + + if (child->IsRelocated()) { + MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash"); + owned->RemoveElement(child); + if (owned->Length() == 0) { + mARIAOwnsHash.Remove(aRoot); + owned = nullptr; + } + } + + // Removing this accessible from the document doesn't mean anything about + // accessibles for subdocuments, so skip removing those from the tree. + if (!child->IsDoc()) { + UncacheChildrenInSubtree(child); + } + } + + if (aRoot->IsNodeMapEntry() && + mNodeToAccessibleMap.Get(aRoot->GetNode()) == aRoot) { + mNodeToAccessibleMap.Remove(aRoot->GetNode()); + } +} + +void DocAccessible::ShutdownChildrenInSubtree(LocalAccessible* aAccessible) { + // Traverse through children and shutdown them before this accessible. When + // child gets shutdown then it removes itself from children array of its + // parent. Use jdx index to process the cases if child is not attached to the + // parent and as result doesn't remove itself from its children. + uint32_t count = aAccessible->ContentChildCount(); + for (uint32_t idx = 0, jdx = 0; idx < count; idx++) { + LocalAccessible* child = aAccessible->ContentChildAt(jdx); + if (!child->IsBoundToParent()) { + NS_ERROR("Parent refers to a child, child doesn't refer to parent!"); + jdx++; + } + + // Don't cross document boundaries. The outerdoc shutdown takes care about + // its subdocument. + if (!child->IsDoc()) ShutdownChildrenInSubtree(child); + } + + UnbindFromDocument(aAccessible); +} + +bool DocAccessible::IsLoadEventTarget() const { + nsCOMPtr<nsIDocShellTreeItem> treeItem = mDocumentNode->GetDocShell(); + if (!treeItem) { + return false; + } + + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem)); + + // Not a root document. + if (parentTreeItem) { + // Return true if it's either: + // a) tab document; + nsCOMPtr<nsIDocShellTreeItem> rootTreeItem; + treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem)); + if (parentTreeItem == rootTreeItem) return true; + + // b) frame/iframe document and its parent document is not in loading state + // Note: we can get notifications while document is loading (and thus + // while there's no parent document yet). + DocAccessible* parentDoc = ParentDocument(); + return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded); + } + + // It's content (not chrome) root document. + return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent); +} + +void DocAccessible::SetIPCDoc(DocAccessibleChild* aIPCDoc) { + MOZ_ASSERT(!mIPCDoc || !aIPCDoc, "Clobbering an attached IPCDoc!"); + mIPCDoc = aIPCDoc; +} + +void DocAccessible::DispatchScrollingEvent(nsINode* aTarget, + uint32_t aEventType) { + LocalAccessible* acc = GetAccessible(aTarget); + if (!acc) { + return; + } + + nsIFrame* frame = acc->GetFrame(); + if (!frame) { + // Although the accessible had a frame at scroll time, it may now be gone + // because of display: contents. + return; + } + + auto [scrollPoint, scrollRange] = ComputeScrollData(acc); + + int32_t appUnitsPerDevPixel = + mPresShell->GetPresContext()->AppUnitsPerDevPixel(); + + LayoutDeviceIntPoint scrollPointDP = LayoutDevicePoint::FromAppUnitsToNearest( + scrollPoint, appUnitsPerDevPixel); + LayoutDeviceIntRect scrollRangeDP = + LayoutDeviceRect::FromAppUnitsToNearest(scrollRange, appUnitsPerDevPixel); + + RefPtr<AccEvent> event = + new AccScrollingEvent(aEventType, acc, scrollPointDP.x, scrollPointDP.y, + scrollRangeDP.width, scrollRangeDP.height); + nsEventShell::FireEvent(event); +} + +void DocAccessible::ARIAActiveDescendantIDMaybeMoved( + LocalAccessible* aAccessible) { + LocalAccessible* widget = nullptr; + if (aAccessible->IsActiveDescendant(&widget) && widget) { + // The active descendant might have just been inserted and may not be in the + // tree yet. Therefore, schedule this async to ensure the tree is up to + // date. + mNotificationController + ->ScheduleNotification<DocAccessible, LocalAccessible>( + this, &DocAccessible::ARIAActiveDescendantChanged, widget); + } +} + +void DocAccessible::SetRoleMapEntryForDoc(dom::Element* aElement) { + const nsRoleMapEntry* entry = aria::GetRoleMap(aElement); + if (!entry || entry->role == roles::APPLICATION || + entry->role == roles::DIALOG || + // Role alert isn't valid on the body element according to the ARIA spec, + // but it's useful for our UI; e.g. the WebRTC sharing indicator. + (entry->role == roles::ALERT && !mDocumentNode->IsContentDocument())) { + SetRoleMapEntry(entry); + return; + } + // No other ARIA roles are valid on body elements. + SetRoleMapEntry(nullptr); +} + +LocalAccessible* DocAccessible::GetAccessible(nsINode* aNode) const { + return aNode == mDocumentNode ? const_cast<DocAccessible*>(this) + : mNodeToAccessibleMap.Get(aNode); +} + +bool DocAccessible::HasPrimaryAction() const { + if (HyperTextAccessible::HasPrimaryAction()) { + return true; + } + // mContent is normally the body, but there might be a click listener on the + // root. + dom::Element* root = mDocumentNode->GetRootElement(); + if (mContent != root) { + return nsCoreUtils::HasClickListener(root); + } + return false; +} + +void DocAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { + aName.Truncate(); + if (aIndex != 0) { + return; + } + if (HasPrimaryAction()) { + aName.AssignLiteral("click"); + } +} + +void DocAccessible::MaybeHandleChangeToHiddenNameOrDescription( + nsIContent* aChild) { + if (!HasLoadState(eTreeConstructed)) { + return; + } + for (nsIContent* content = aChild; content; content = content->GetParent()) { + if (HasAccessible(content)) { + // This node isn't hidden. Events for name/description dependents will be + // fired elsewhere. + break; + } + nsAtom* id = content->GetID(); + if (!id) { + continue; + } + auto* providers = + GetRelProviders(content->AsElement(), nsDependentAtomString(id)); + if (!providers) { + continue; + } + for (auto& provider : *providers) { + if (provider->mRelAttr != nsGkAtoms::aria_labelledby && + provider->mRelAttr != nsGkAtoms::aria_describedby) { + continue; + } + LocalAccessible* dependentAcc = GetAccessible(provider->mContent); + if (!dependentAcc) { + continue; + } + FireDelayedEvent(provider->mRelAttr == nsGkAtoms::aria_labelledby + ? nsIAccessibleEvent::EVENT_NAME_CHANGE + : nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, + dependentAcc); + } + } +} diff --git a/accessible/generic/DocAccessible.h b/accessible/generic/DocAccessible.h new file mode 100644 index 0000000000..f0453ce766 --- /dev/null +++ b/accessible/generic/DocAccessible.h @@ -0,0 +1,827 @@ +/* -*- 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_DocAccessible_h__ +#define mozilla_a11y_DocAccessible_h__ + +#include "nsIAccessiblePivot.h" + +#include "HyperTextAccessibleWrap.h" +#include "AccEvent.h" + +#include "nsClassHashtable.h" +#include "nsTHashMap.h" +#include "mozilla/UniquePtr.h" +#include "nsIDocumentObserver.h" +#include "nsITimer.h" +#include "nsTHashSet.h" +#include "nsWeakReference.h" + +class nsAccessiblePivot; + +const uint32_t kDefaultCacheLength = 128; + +namespace mozilla { + +class EditorBase; +class PresShell; + +namespace dom { +class Document; +} + +namespace a11y { + +class DocManager; +class NotificationController; +class DocAccessibleChild; +class RelatedAccIterator; +template <class Class, class... Args> +class TNotification; + +/** + * An accessibility tree node that originated in a content process and + * represents a document. Tabs, in-process iframes, and out-of-process iframes + * all use this class to represent the doc they contain. + */ +class DocAccessible : public HyperTextAccessibleWrap, + public nsIDocumentObserver, + public nsSupportsWeakReference, + public nsIAccessiblePivotObserver { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DocAccessible, LocalAccessible) + + NS_DECL_NSIACCESSIBLEPIVOTOBSERVER + + protected: + typedef mozilla::dom::Document Document; + + public: + DocAccessible(Document* aDocument, PresShell* aPresShell); + + // nsIDocumentObserver + NS_DECL_NSIDOCUMENTOBSERVER + + // LocalAccessible + virtual void Init(); + virtual void Shutdown() override; + virtual nsIFrame* GetFrame() const override; + virtual nsINode* GetNode() const override; + Document* DocumentNode() const { return mDocumentNode; } + + virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) const override; + virtual void Description(nsString& aDescription) const override; + virtual Accessible* FocusedChild() override; + virtual mozilla::a11y::role NativeRole() const override; + virtual uint64_t NativeState() const override; + virtual uint64_t NativeInteractiveState() const override; + virtual bool NativelyUnavailable() const override; + virtual void ApplyARIAState(uint64_t* aState) const override; + + virtual void TakeFocus() const override; + +#ifdef A11Y_LOG + virtual nsresult HandleAccEvent(AccEvent* aEvent) override; +#endif + + virtual nsRect RelativeBounds(nsIFrame** aRelativeFrame) const override; + + // ActionAccessible + virtual bool HasPrimaryAction() const override; + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + + // HyperTextAccessible + virtual already_AddRefed<EditorBase> GetEditor() const override; + + // DocAccessible + + /** + * Return document URL. + */ + void URL(nsAString& aURL) const; + + /** + * Return DOM document title. + */ + void Title(nsString& aTitle) const; + + /** + * Return DOM document mime type. + */ + void MimeType(nsAString& aType) const; + /** + * Return DOM document type. + */ + void DocType(nsAString& aType) const; + + /** + * Adds an entry to mQueuedCacheUpdates indicating aAcc requires + * a cache update on domain aNewDomain. If we've already queued an update + * for aAcc, aNewDomain is or'd with the existing domain(s) + * and the map is updated. Otherwise, the entry is simply inserted. + * This function also schedules processing on the controller. + * Note that this CANNOT be used for anything which fires events, since events + * must be fired after their associated cache update. + */ + void QueueCacheUpdate(LocalAccessible* aAcc, uint64_t aNewDomain); + + /** + * Walks the mDependentIDsHashes list for the given accessible and + * queues a CacheDomain::Relations cache update fore each related acc. + * We call this when we observe an ID mutation or when an acc is bound + * to its document. + */ + void QueueCacheUpdateForDependentRelations(LocalAccessible* aAcc); + + /** + * Return virtual cursor associated with the document. + */ + nsIAccessiblePivot* VirtualCursor(); + + /** + * Returns true if the instance has shutdown. + */ + bool HasShutdown() const { return !mPresShell; } + + /** + * Return presentation shell for this document accessible. + */ + PresShell* PresShellPtr() const { + MOZ_DIAGNOSTIC_ASSERT(!HasShutdown()); + return mPresShell; + } + + /** + * Return the presentation shell's context. + */ + nsPresContext* PresContext() const; + + /** + * Return true if associated DOM document was loaded and isn't unloading. + */ + bool IsContentLoaded() const; + + bool IsHidden() const; + + void SetViewportCacheDirty(bool aDirty) { mViewportCacheDirty = aDirty; } + + /** + * Document load states. + */ + enum LoadState { + // initial tree construction is pending + eTreeConstructionPending = 0, + // initial tree construction done + eTreeConstructed = 1, + // DOM document is loaded. + eDOMLoaded = 1 << 1, + // document is ready + eReady = eTreeConstructed | eDOMLoaded, + // document and all its subdocuments are ready + eCompletelyLoaded = eReady | 1 << 2 + }; + + /** + * Return true if the document has given document state. + */ + bool HasLoadState(LoadState aState) const { + return (mLoadState & static_cast<uint32_t>(aState)) == + static_cast<uint32_t>(aState); + } + + /** + * Return a native window handler or pointer depending on platform. + */ + virtual void* GetNativeWindow() const; + + /** + * Return the parent document. + */ + DocAccessible* ParentDocument() const { + return mParent ? mParent->Document() : nullptr; + } + + /** + * Return the child document count. + */ + uint32_t ChildDocumentCount() const { return mChildDocuments.Length(); } + + /** + * Return the child document at the given index. + */ + DocAccessible* GetChildDocumentAt(uint32_t aIndex) const { + return mChildDocuments.SafeElementAt(aIndex, nullptr); + } + + /** + * Fire accessible event asynchronously. + */ + void FireDelayedEvent(AccEvent* aEvent); + void FireDelayedEvent(uint32_t aEventType, LocalAccessible* aTarget); + void FireEventsOnInsertion(LocalAccessible* aContainer); + + /** + * Fire value change event on the given accessible if applicable. + */ + void MaybeNotifyOfValueChange(LocalAccessible* aAccessible); + + /** + * Get/set the anchor jump. + */ + LocalAccessible* AnchorJump() { + return GetAccessibleOrContainer(mAnchorJumpElm); + } + + void SetAnchorJump(nsIContent* aTargetNode) { mAnchorJumpElm = aTargetNode; } + + /** + * Bind the child document to the tree. + */ + void BindChildDocument(DocAccessible* aDocument); + + /** + * Process the generic notification. + * + * @note The caller must guarantee that the given instance still exists when + * notification is processed. + * @see NotificationController::HandleNotification + */ + template <class Class, class... Args> + void HandleNotification( + Class* aInstance, + typename TNotification<Class, Args...>::Callback aMethod, Args*... aArgs); + + /** + * Return the cached accessible by the given DOM node if it's in subtree of + * this document accessible or the document accessible itself, otherwise null. + * + * @return the accessible object + */ + LocalAccessible* GetAccessible(nsINode* aNode) const; + + /** + * Return an accessible for the given node even if the node is not in + * document's node map cache (like HTML area element). + * + * XXX: it should be really merged with GetAccessible(). + */ + LocalAccessible* GetAccessibleEvenIfNotInMap(nsINode* aNode) const; + LocalAccessible* GetAccessibleEvenIfNotInMapOrContainer(nsINode* aNode) const; + + /** + * Return whether the given DOM node has an accessible or not. + */ + bool HasAccessible(nsINode* aNode) const { return GetAccessible(aNode); } + + /** + * Return the cached accessible by the given unique ID within this document. + * + * @note the unique ID matches with the uniqueID() of Accessible + * + * @param aUniqueID [in] the unique ID used to cache the node. + */ + LocalAccessible* GetAccessibleByUniqueID(void* aUniqueID) { + return UniqueID() == aUniqueID ? this : mAccessibleCache.GetWeak(aUniqueID); + } + + /** + * Return the cached accessible by the given unique ID looking through + * this and nested documents. + */ + LocalAccessible* GetAccessibleByUniqueIDInSubtree(void* aUniqueID); + + /** + * Return an accessible for the given DOM node or container accessible if + * the node is not accessible. If aNoContainerIfPruned is true it will return + * null if the node is in a pruned subtree (eg. aria-hidden or unselected deck + * panel) + */ + LocalAccessible* GetAccessibleOrContainer( + nsINode* aNode, bool aNoContainerIfPruned = false) const; + + /** + * Return a container accessible for the given DOM node. + */ + LocalAccessible* GetContainerAccessible(nsINode* aNode) const; + + /** + * Return an accessible for the given node if any, or an immediate accessible + * container for it. + */ + LocalAccessible* AccessibleOrTrueContainer( + nsINode* aNode, bool aNoContainerIfPruned = false) const; + + /** + * Return an accessible for the given node or its first accessible descendant. + */ + LocalAccessible* GetAccessibleOrDescendant(nsINode* aNode) const; + + /** + * Returns aria-owns seized child at the given index. + */ + LocalAccessible* ARIAOwnedAt(LocalAccessible* aParent, + uint32_t aIndex) const { + nsTArray<RefPtr<LocalAccessible>>* children = mARIAOwnsHash.Get(aParent); + if (children) { + return children->SafeElementAt(aIndex); + } + return nullptr; + } + uint32_t ARIAOwnedCount(LocalAccessible* aParent) const { + nsTArray<RefPtr<LocalAccessible>>* children = mARIAOwnsHash.Get(aParent); + return children ? children->Length() : 0; + } + + /** + * Return true if the given ID is referred by relation attribute. + */ + bool IsDependentID(dom::Element* aElement, const nsAString& aID) const { + return GetRelProviders(aElement, aID); + } + + /** + * Initialize the newly created accessible and put it into document caches. + * + * @param aAccessible [in] created accessible + * @param aRoleMapEntry [in] the role map entry role the ARIA role or + * nullptr if none + */ + void BindToDocument(LocalAccessible* aAccessible, + const nsRoleMapEntry* aRoleMapEntry); + + /** + * Remove from document and shutdown the given accessible. + */ + void UnbindFromDocument(LocalAccessible* aAccessible); + + /** + * Notify the document accessible that content was inserted. + */ + void ContentInserted(nsIContent* aStartChildNode, nsIContent* aEndChildNode); + + /** + * @see nsAccessibilityService::ScheduleAccessibilitySubtreeUpdate + */ + void ScheduleTreeUpdate(nsIContent* aContent); + + /** + * Update the tree on content removal. + */ + void ContentRemoved(LocalAccessible* aAccessible); + void ContentRemoved(nsIContent* aContentNode); + + /** + * Updates accessible tree when rendered text is changed. + */ + void UpdateText(nsIContent* aTextNode); + + /** + * Recreate an accessible, results in hide/show events pair. + */ + void RecreateAccessible(nsIContent* aContent); + + /** + * Schedule ARIA owned element relocation if needed. Return true if relocation + * was scheduled. + */ + bool RelocateARIAOwnedIfNeeded(nsIContent* aEl); + + /** + * Return a notification controller associated with the document. + */ + NotificationController* Controller() const { return mNotificationController; } + + /** + * If this document is in a content process return the object responsible for + * communicating with the main process for it. + */ + DocAccessibleChild* IPCDoc() const { return mIPCDoc; } + + /** + * Notify the document that a DOM node has been scrolled. document will + * dispatch throttled accessibility events for scrolling, and a scroll-end + * event. This function also queues a cache update for ScrollPosition. + */ + void HandleScroll(nsINode* aTarget); + + /** + * Retrieves the scroll frame (if it exists) for the given accessible + * and returns its scroll position and scroll range. If the given + * accessible is `this`, return the scroll position and range of + * the root scroll frame. Return values have been scaled by the + * PresShell's resolution. + */ + std::pair<nsPoint, nsRect> ComputeScrollData(LocalAccessible* aAcc); + + protected: + virtual ~DocAccessible(); + + void LastRelease(); + + // DocAccessible + virtual nsresult AddEventListeners(); + virtual nsresult RemoveEventListeners(); + + /** + * Marks this document as loaded or loading. + */ + void NotifyOfLoad(uint32_t aLoadEventType); + void NotifyOfLoading(bool aIsReloading); + + friend class DocManager; + + /** + * Perform initial update (create accessible tree). + * Can be overridden by wrappers to prepare initialization work. + */ + virtual void DoInitialUpdate(); + + /** + * Updates root element and picks up ARIA role on it if any. + */ + void UpdateRootElIfNeeded(); + + /** + * Process document load notification, fire document load and state busy + * events if applicable. + */ + void ProcessLoad(); + + /** + * Append the given document accessible to this document's child document + * accessibles. + */ + bool AppendChildDocument(DocAccessible* aChildDocument) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + mChildDocuments.AppendElement(aChildDocument); + return true; + } + + /** + * Remove the given document accessible from this document's child document + * accessibles. + */ + void RemoveChildDocument(DocAccessible* aChildDocument) { + mChildDocuments.RemoveElement(aChildDocument); + } + + /** + * Add dependent IDs pointed by accessible element by relation attribute to + * cache. If the relation attribute is missed then all relation attributes + * are checked. + * + * @param aRelProvider [in] accessible that element has relation attribute + * @param aRelAttr [in, optional] relation attribute + */ + void AddDependentIDsFor(LocalAccessible* aRelProvider, + nsAtom* aRelAttr = nullptr); + + /** + * Remove dependent IDs pointed by accessible element by relation attribute + * from cache. If the relation attribute is absent then all relation + * attributes are checked. + * + * @param aRelProvider [in] accessible that element has relation attribute + * @param aRelAttr [in, optional] relation attribute + */ + void RemoveDependentIDsFor(LocalAccessible* aRelProvider, + nsAtom* aRelAttr = nullptr); + + /** + * Update or recreate an accessible depending on a changed attribute. + * + * @param aElement [in] the element the attribute was changed on + * @param aAttribute [in] the changed attribute + * @return true if an action was taken on the attribute change + */ + bool UpdateAccessibleOnAttrChange(mozilla::dom::Element* aElement, + nsAtom* aAttribute); + + /** + * Process ARIA active-descendant attribute change. + */ + void ARIAActiveDescendantChanged(LocalAccessible* aAccessible); + + /** + * Update the accessible tree for inserted content. + */ + void ProcessContentInserted( + LocalAccessible* aContainer, + const nsTArray<nsCOMPtr<nsIContent>>* aInsertedContent); + void ProcessContentInserted(LocalAccessible* aContainer, + nsIContent* aInsertedContent); + + /** + * Used to notify the document to make it process the invalidation list. + * + * While children are cached we may encounter the case there's no accessible + * for referred content by related accessible. Store these related nodes to + * invalidate their containers later. + */ + void ProcessInvalidationList(); + + /** + * Process mPendingUpdates + */ + void ProcessPendingUpdates(); + + /** + * Called from NotificationController to process this doc's + * mQueuedCacheUpdates list. For each acc in the map, this function + * sends a cache update with its corresponding CacheDomain. + */ + void ProcessQueuedCacheUpdates(); + + /** + * Only works in content process documents. + */ + bool IsAccessibleBeingMoved(LocalAccessible* aAcc) { + return mMovedAccessibles.Contains(aAcc); + } + + /** + * Called from NotificationController before mutation events are processed to + * notify the parent process which Accessibles are being moved (if any). + */ + void SendAccessiblesWillMove(); + + /** + * Called from NotificationController after all mutation events have been + * processed to clear our data about Accessibles that were moved during this + * tick. + */ + void ClearMovedAccessibles() { + mMovedAccessibles.Clear(); + mInsertedAccessibles.Clear(); + } + + /** + * Steals or puts back accessible subtrees. + */ + void DoARIAOwnsRelocation(LocalAccessible* aOwner); + + /** + * Moves children back under their original parents. + */ + void PutChildrenBack(nsTArray<RefPtr<LocalAccessible>>* aChildren, + uint32_t aStartIdx); + + bool MoveChild(LocalAccessible* aChild, LocalAccessible* aNewParent, + int32_t aIdxInParent); + + /** + * Create accessible tree. + * + * @param aRoot [in] a root of subtree to create + * @param aFocusedAcc [in, optional] a focused accessible under created + * subtree if any + */ + void CacheChildrenInSubtree(LocalAccessible* aRoot, + LocalAccessible** aFocusedAcc = nullptr); + void CreateSubtree(LocalAccessible* aRoot); + + /** + * Remove accessibles in subtree from node to accessible map. + */ + void UncacheChildrenInSubtree(LocalAccessible* aRoot); + + /** + * Shutdown any cached accessible in the subtree. + * + * @param aAccessible [in] the root of the subrtee to invalidate accessible + * child/parent refs in + */ + void ShutdownChildrenInSubtree(LocalAccessible* aAccessible); + + /** + * Return true if the document is a target of document loading events + * (for example, state busy change or document reload events). + * + * Rules: The root chrome document accessible is never an event target + * (for example, Firefox UI window). If the sub document is loaded within its + * parent document then the parent document is a target only (aka events + * coalescence). + */ + bool IsLoadEventTarget() const; + + /* + * Set the object responsible for communicating with the main process on + * behalf of this document. + */ + void SetIPCDoc(DocAccessibleChild* aIPCDoc); + + friend class DocAccessibleChildBase; + + /** + * Used to fire scrolling end event after page scroll. + * + * @param aTimer [in] the timer object + * @param aClosure [in] the document accessible where scrolling happens + */ + static void ScrollTimerCallback(nsITimer* aTimer, void* aClosure); + + void DispatchScrollingEvent(nsINode* aTarget, uint32_t aEventType); + + /** + * Check if an id attribute change affects aria-activedescendant and handle + * the aria-activedescendant change if appropriate. + * If the currently focused element has aria-activedescendant and an + * element's id changes to match this, the id was probably moved from the + * previous active descendant, thus making this element the new active + * descendant. In that case, accessible focus must be changed accordingly. + */ + void ARIAActiveDescendantIDMaybeMoved(LocalAccessible* aAccessible); + + /** + * Traverse content subtree and for each node do one of 3 things: + * 1. Check if content node has an accessible that should be removed and + * remove it. + * 2. Check if content node has an accessible that needs to be recreated. + * Remove it and schedule it for reinsertion. + * 3. Check if content node has no accessible but needs one. Schedule one for + * insertion. + * + * Returns true if the root node should be reinserted. + */ + bool PruneOrInsertSubtree(nsIContent* aRoot); + + protected: + /** + * State and property flags, kept by mDocFlags. + */ + enum { + // Whether the document is a top level content document in this process. + eTopLevelContentDocInProcess = 1 << 0 + }; + + /** + * Cache of accessibles within this document accessible. + */ + AccessibleHashtable mAccessibleCache; + nsTHashMap<nsPtrHashKey<const nsINode>, LocalAccessible*> + mNodeToAccessibleMap; + + Document* mDocumentNode; + nsCOMPtr<nsITimer> mScrollWatchTimer; + nsTHashMap<nsPtrHashKey<nsINode>, TimeStamp> mLastScrollingDispatch; + + /** + * Bit mask of document load states (@see LoadState). + */ + uint32_t mLoadState : 3; + + /** + * Bit mask of other states and props. + */ + uint32_t mDocFlags : 27; + + /** + * Tracks whether we have seen changes to this document's content that + * indicate we should re-send the viewport cache we use for hittesting. + * This value is set in `BundleFieldsForCache` and processed in + * `ProcessQueuedCacheUpdates`. + */ + bool mViewportCacheDirty : 1; + + /** + * Type of document load event fired after the document is loaded completely. + */ + uint32_t mLoadEventType; + + /** + * Reference to anchor jump element. + */ + nsCOMPtr<nsIContent> mAnchorJumpElm; + + /** + * A generic state (see items below) before the attribute value was changed. + * @see AttributeWillChange and AttributeChanged notifications. + */ + + // Previous state bits before attribute change + uint64_t mPrevStateBits; + + nsTArray<RefPtr<DocAccessible>> mChildDocuments; + + /** + * The virtual cursor of the document. + */ + RefPtr<nsAccessiblePivot> mVirtualCursor; + + /** + * A storage class for pairing content with one of its relation attributes. + */ + class AttrRelProvider { + public: + AttrRelProvider(nsAtom* aRelAttr, nsIContent* aContent) + : mRelAttr(aRelAttr), mContent(aContent) {} + + nsAtom* mRelAttr; + nsCOMPtr<nsIContent> mContent; + + private: + AttrRelProvider(); + AttrRelProvider(const AttrRelProvider&); + AttrRelProvider& operator=(const AttrRelProvider&); + }; + + typedef nsTArray<mozilla::UniquePtr<AttrRelProvider>> AttrRelProviders; + typedef nsClassHashtable<nsStringHashKey, AttrRelProviders> + DependentIDsHashtable; + + /** + * Returns/creates/removes attribute relation providers associated with + * a DOM document if the element is in uncomposed document or associated + * with shadow DOM the element is in. + */ + AttrRelProviders* GetRelProviders(dom::Element* aElement, + const nsAString& aID) const; + AttrRelProviders* GetOrCreateRelProviders(dom::Element* aElement, + const nsAString& aID); + void RemoveRelProvidersIfEmpty(dom::Element* aElement, const nsAString& aID); + + /** + * The cache of IDs pointed by relation attributes. + */ + nsClassHashtable<nsPtrHashKey<dom::DocumentOrShadowRoot>, + DependentIDsHashtable> + mDependentIDsHashes; + + friend class RelatedAccIterator; + + /** + * Used for our caching algorithm. We store the list of nodes that should be + * invalidated. + * + * @see ProcessInvalidationList + */ + nsTArray<RefPtr<nsIContent>> mInvalidationList; + + /** + * Holds a list of aria-owns relocations. + */ + nsClassHashtable<nsPtrHashKey<LocalAccessible>, + nsTArray<RefPtr<LocalAccessible>>> + mARIAOwnsHash; + + /** + * Keeps a list of pending subtrees to update post-refresh. + */ + nsTArray<RefPtr<nsIContent>> mPendingUpdates; + + /** + * Used to process notification from core and accessible events. + */ + RefPtr<NotificationController> mNotificationController; + friend class EventTree; + friend class NotificationController; + + private: + void SetRoleMapEntryForDoc(dom::Element* aElement); + + /** + * This must be called whenever an Accessible is moved in a content process. + * It keeps track of Accessibles moved during this tick. + */ + void TrackMovedAccessible(LocalAccessible* aAcc); + + /** + * For hidden subtrees, fire a name/description change event if the subtree + * is a target of aria-labelledby/describedby. + * This does nothing if it is called on a node which is not part of a hidden + * aria-labelledby/describedby target. + */ + void MaybeHandleChangeToHiddenNameOrDescription(nsIContent* aChild); + + PresShell* mPresShell; + + // Exclusively owned by IPDL so don't manually delete it! + DocAccessibleChild* mIPCDoc; + + // A hash map between LocalAccessibles and CacheDomains, tracking + // cache updates that have been queued during the current tick + // but not yet sent. It is possible for this map to contain a reference + // to the document it lives on. We clear the list in Shutdown() to + // avoid cyclical references. + nsTHashMap<RefPtr<LocalAccessible>, uint64_t> mQueuedCacheUpdates; + + // A set of Accessibles moved during this tick. Only used in content + // processes. + nsTHashSet<RefPtr<LocalAccessible>> mMovedAccessibles; + // A set of Accessibles inserted during this tick. Only used in content + // processes. This is needed to prevent insertions + moves of the same + // Accessible in the same tick from being tracked as moves. + nsTHashSet<RefPtr<LocalAccessible>> mInsertedAccessibles; +}; + +inline DocAccessible* LocalAccessible::AsDoc() { + return IsDoc() ? static_cast<DocAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/FormControlAccessible.cpp b/accessible/generic/FormControlAccessible.cpp new file mode 100644 index 0000000000..91cc9fab6d --- /dev/null +++ b/accessible/generic/FormControlAccessible.cpp @@ -0,0 +1,84 @@ +/* -*- 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/. */ + +// NOTE: alphabetically ordered + +#include "FormControlAccessible.h" + +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/FloatingPoint.h" +#include "Role.h" + +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// CheckboxAccessible +//////////////////////////////////////////////////////////////////////////////// + +role CheckboxAccessible::NativeRole() const { return roles::CHECKBUTTON; } + +void CheckboxAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { + if (aIndex == eAction_Click) { + uint64_t state = NativeState(); + if (state & states::CHECKED) { + aName.AssignLiteral("uncheck"); + } else if (state & states::MIXED) { + aName.AssignLiteral("cycle"); + } else { + aName.AssignLiteral("check"); + } + } +} + +bool CheckboxAccessible::HasPrimaryAction() const { return true; } + +uint64_t CheckboxAccessible::NativeState() const { + uint64_t state = LeafAccessible::NativeState(); + + state |= states::CHECKABLE; + dom::HTMLInputElement* input = dom::HTMLInputElement::FromNode(mContent); + if (input) { // HTML:input@type="checkbox" + if (input->Indeterminate()) { + return state | states::MIXED; + } + + if (input->Checked()) { + return state | states::CHECKED; + } + + } else if (mContent->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true, + eCaseMatters)) { // XUL checkbox + return state | states::CHECKED; + } + + return state; +} + +//////////////////////////////////////////////////////////////////////////////// +// CheckboxAccessible: Widgets + +bool CheckboxAccessible::IsWidget() const { return true; } + +//////////////////////////////////////////////////////////////////////////////// +// RadioButtonAccessible +//////////////////////////////////////////////////////////////////////////////// + +RadioButtonAccessible::RadioButtonAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : LeafAccessible(aContent, aDoc) {} + +bool RadioButtonAccessible::HasPrimaryAction() const { return true; } + +void RadioButtonAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { + if (aIndex == eAction_Click) aName.AssignLiteral("select"); +} + +role RadioButtonAccessible::NativeRole() const { return roles::RADIOBUTTON; } + +//////////////////////////////////////////////////////////////////////////////// +// RadioButtonAccessible: Widgets + +bool RadioButtonAccessible::IsWidget() const { return true; } diff --git a/accessible/generic/FormControlAccessible.h b/accessible/generic/FormControlAccessible.h new file mode 100644 index 0000000000..44c142d4e2 --- /dev/null +++ b/accessible/generic/FormControlAccessible.h @@ -0,0 +1,65 @@ +/* -*- 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_FormControlAccessible_H_ +#define MOZILLA_A11Y_FormControlAccessible_H_ + +#include "BaseAccessibles.h" + +namespace mozilla { +namespace a11y { + +/** + * Checkbox accessible. + */ +class CheckboxAccessible : public LeafAccessible { + public: + enum { eAction_Click = 0 }; + + CheckboxAccessible(nsIContent* aContent, DocAccessible* aDoc) + : LeafAccessible(aContent, aDoc) { + // Ignore "CheckboxStateChange" DOM event in lieu of document observer + // state change notification. + if (aContent->IsHTMLElement()) { + mStateFlags |= eIgnoreDOMUIEvent; + } + } + + // LocalAccessible + virtual mozilla::a11y::role NativeRole() const override; + virtual uint64_t NativeState() const override; + + // ActionAccessible + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + virtual bool HasPrimaryAction() const override; + + // Widgets + virtual bool IsWidget() const override; +}; + +/** + * Generic class used for radio buttons. + */ +class RadioButtonAccessible : public LeafAccessible { + public: + RadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc); + + // LocalAccessible + virtual mozilla::a11y::role NativeRole() const override; + + // ActionAccessible + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + virtual bool HasPrimaryAction() const override; + + enum { eAction_Click = 0 }; + + // Widgets + virtual bool IsWidget() const override; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/HyperTextAccessible-inl.h b/accessible/generic/HyperTextAccessible-inl.h new file mode 100644 index 0000000000..3bb0530c9c --- /dev/null +++ b/accessible/generic/HyperTextAccessible-inl.h @@ -0,0 +1,129 @@ +/* -*- 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_HyperTextAccessible_inl_h__ +#define mozilla_a11y_HyperTextAccessible_inl_h__ + +#include "HyperTextAccessible.h" + +#include "nsAccUtils.h" + +#include "nsIClipboard.h" +#include "nsFrameSelection.h" + +#include "mozilla/EditorBase.h" + +namespace mozilla { +namespace a11y { + +inline void HyperTextAccessible::SetCaretOffset(int32_t aOffset) { + SetSelectionRange(aOffset, aOffset); + // XXX: Force cache refresh until a good solution for AT emulation of user + // input is implemented (AccessFu caret movement). + SelectionMgr()->UpdateCaretOffset(this, aOffset); +} + +inline bool HyperTextAccessible::AddToSelection(int32_t aStartOffset, + int32_t aEndOffset) { + dom::Selection* domSel = DOMSelection(); + return domSel && + SetSelectionBoundsAt(domSel->RangeCount(), aStartOffset, aEndOffset); +} + +inline void HyperTextAccessible::ReplaceText(const nsAString& aText) { + if (aText.Length() == 0) { + DeleteText(0, CharacterCount()); + return; + } + + SetSelectionRange(0, CharacterCount()); + + RefPtr<EditorBase> editorBase = GetEditor(); + if (!editorBase) { + return; + } + + DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text"); +} + +inline void HyperTextAccessible::InsertText(const nsAString& aText, + int32_t aPosition) { + RefPtr<EditorBase> editorBase = GetEditor(); + if (editorBase) { + SetSelectionRange(aPosition, aPosition); + DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the text"); + } +} + +inline void HyperTextAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) { + RefPtr<EditorBase> editorBase = GetEditor(); + if (editorBase) { + SetSelectionRange(aStartPos, aEndPos); + editorBase->Copy(); + } +} + +inline void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) { + RefPtr<EditorBase> editorBase = GetEditor(); + if (editorBase) { + SetSelectionRange(aStartPos, aEndPos); + editorBase->Cut(); + } +} + +inline void HyperTextAccessible::DeleteText(int32_t aStartPos, + int32_t aEndPos) { + RefPtr<EditorBase> editorBase = GetEditor(); + if (!editorBase) { + return; + } + SetSelectionRange(aStartPos, aEndPos); + DebugOnly<nsresult> rv = + editorBase->DeleteSelectionAsAction(nsIEditor::eNone, nsIEditor::eStrip); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to delete text"); +} + +inline void HyperTextAccessible::PasteText(int32_t aPosition) { + RefPtr<EditorBase> editorBase = GetEditor(); + if (editorBase) { + SetSelectionRange(aPosition, aPosition); + editorBase->PasteAsAction(nsIClipboard::kGlobalClipboard, true); + } +} + +inline uint32_t HyperTextAccessible::AdjustCaretOffset(uint32_t aOffset) const { + // 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. + if (aOffset > 0 && IsCaretAtEndOfLine()) return aOffset - 1; + + return aOffset; +} + +inline bool HyperTextAccessible::IsCaretAtEndOfLine() const { + RefPtr<nsFrameSelection> frameSelection = FrameSelection(); + return frameSelection && frameSelection->GetHint() == CARET_ASSOCIATE_BEFORE; +} + +inline already_AddRefed<nsFrameSelection> HyperTextAccessible::FrameSelection() + const { + nsIFrame* frame = GetFrame(); + return frame ? frame->GetFrameSelection() : nullptr; +} + +inline dom::Selection* HyperTextAccessible::DOMSelection() const { + RefPtr<nsFrameSelection> frameSelection = FrameSelection(); + return frameSelection ? frameSelection->GetSelection(SelectionType::eNormal) + : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/HyperTextAccessible.cpp b/accessible/generic/HyperTextAccessible.cpp new file mode 100644 index 0000000000..fec1b31b28 --- /dev/null +++ b/accessible/generic/HyperTextAccessible.cpp @@ -0,0 +1,2357 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 et tw=78: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "HyperTextAccessible-inl.h" + +#include "nsAccessibilityService.h" +#include "nsAccessiblePivot.h" +#include "nsIAccessibleTypes.h" +#include "AccAttributes.h" +#include "DocAccessible.h" +#include "HTMLListAccessible.h" +#include "LocalAccessible-inl.h" +#include "Pivot.h" +#include "Relation.h" +#include "Role.h" +#include "States.h" +#include "TextAttrs.h" +#include "TextLeafRange.h" +#include "TextRange.h" +#include "TreeWalker.h" + +#include "nsCaret.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsFocusManager.h" +#include "nsIEditingSession.h" +#include "nsContainerFrame.h" +#include "nsFrameSelection.h" +#include "nsILineIterator.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIScrollableFrame.h" +#include "nsIMathMLFrame.h" +#include "nsRange.h" +#include "nsTextFragment.h" +#include "mozilla/Assertions.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/EditorBase.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLBRElement.h" +#include "mozilla/dom/HTMLHeadingElement.h" +#include "mozilla/dom/Selection.h" +#include "gfxSkipChars.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::a11y; + +/** + * This class is used in HyperTextAccessible to search for paragraph + * boundaries. + */ +class ParagraphBoundaryRule : public PivotRule { + public: + explicit ParagraphBoundaryRule(LocalAccessible* aAnchor, + uint32_t aAnchorTextoffset, + nsDirection aDirection, + bool aSkipAnchorSubtree = false) + : mAnchor(aAnchor), + mAnchorTextOffset(aAnchorTextoffset), + mDirection(aDirection), + mSkipAnchorSubtree(aSkipAnchorSubtree), + mLastMatchTextOffset(0) {} + + virtual uint16_t Match(Accessible* aAcc) override { + MOZ_ASSERT(aAcc && aAcc->IsLocal()); + LocalAccessible* acc = aAcc->AsLocal(); + if (acc->IsOuterDoc()) { + // The child document might be remote and we can't (and don't want to) + // handle remote documents. Also, iframes are inline anyway and thus + // can't be paragraph boundaries. Therefore, skip this unconditionally. + return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + if (mSkipAnchorSubtree && acc == mAnchor) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + // First, deal with the case that we encountered a line break, for example, + // a br in a paragraph. + if (acc->Role() == roles::WHITESPACE) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + return result; + } + + // Now, deal with the case that we encounter a new block level accessible. + // This also means a new paragraph boundary start. + nsIFrame* frame = acc->GetFrame(); + if (frame && frame->IsBlockFrame() && + acc->Role() != roles::LISTITEM_MARKER) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + return result; + } + + // A text leaf can contain a line break if it's pre-formatted text. + if (acc->IsTextLeaf()) { + nsAutoString name; + acc->Name(name); + int32_t offset; + if (mDirection == eDirPrevious) { + if (acc == mAnchor && mAnchorTextOffset == 0) { + // We're already at the start of this node, so there can be no line + // break before. + return result; + } + // If we began on a line break, we don't want to match it, so search + // from 1 before our anchor offset. + offset = + name.RFindChar('\n', acc == mAnchor ? mAnchorTextOffset - 1 : -1); + } else { + offset = name.FindChar('\n', acc == mAnchor ? mAnchorTextOffset : 0); + } + if (offset != -1) { + // Line ebreak! + mLastMatchTextOffset = offset; + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; + } + + // This is only valid if the last match was a text leaf. It returns the + // offset of the line break character in that text leaf. + uint32_t GetLastMatchTextOffset() { return mLastMatchTextOffset; } + + private: + LocalAccessible* mAnchor; + uint32_t mAnchorTextOffset; + nsDirection mDirection; + bool mSkipAnchorSubtree; + uint32_t mLastMatchTextOffset; +}; + +/** + * This class is used in HyperTextAccessible::FindParagraphStartOffset to + * search forward exactly one step from a match found by the above. + * It should only be initialized with a boundary, and it will skip that + * boundary's sub tree if it is a block element boundary. + */ +class SkipParagraphBoundaryRule : public PivotRule { + public: + explicit SkipParagraphBoundaryRule(Accessible* aBoundary) + : mBoundary(aBoundary) {} + + virtual uint16_t Match(Accessible* aAcc) override { + MOZ_ASSERT(aAcc && aAcc->IsLocal()); + // If matching the boundary, skip its sub tree. + if (aAcc == mBoundary) { + return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + + private: + Accessible* mBoundary; +}; + +//////////////////////////////////////////////////////////////////////////////// +// HyperTextAccessible +//////////////////////////////////////////////////////////////////////////////// + +HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc) + : AccessibleWrap(aNode, aDoc) { + mType = eHyperTextType; + mGenericTypes |= eHyperText; +} + +role HyperTextAccessible::NativeRole() const { + a11y::role r = GetAccService()->MarkupRole(mContent); + if (r != roles::NOTHING) return r; + + nsIFrame* frame = GetFrame(); + if (frame && frame->IsInlineFrame()) return roles::TEXT; + + return roles::TEXT_CONTAINER; +} + +uint64_t HyperTextAccessible::NativeState() const { + uint64_t states = AccessibleWrap::NativeState(); + + if (IsEditable()) { + states |= states::EDITABLE; + + } else if (mContent->IsHTMLElement(nsGkAtoms::article)) { + // We want <article> to behave like a document in terms of readonly state. + states |= states::READONLY; + } + + nsIFrame* frame = GetFrame(); + if ((states & states::EDITABLE) || (frame && frame->IsSelectable(nullptr))) { + // If the accessible is editable the layout selectable state only disables + // mouse selection, but keyboard (shift+arrow) selection is still possible. + states |= states::SELECTABLE_TEXT; + } + + return states; +} + +bool HyperTextAccessible::IsEditable() const { + if (!mContent) { + return false; + } + return mContent->AsElement()->State().HasState(dom::ElementState::READWRITE); +} + +LayoutDeviceIntRect HyperTextAccessible::GetBoundsInFrame( + nsIFrame* aFrame, uint32_t aStartRenderedOffset, + uint32_t aEndRenderedOffset) { + nsPresContext* presContext = mDoc->PresContext(); + if (!aFrame->IsTextFrame()) { + return LayoutDeviceIntRect::FromAppUnitsToNearest( + aFrame->GetScreenRectInAppUnits(), presContext->AppUnitsPerDevPixel()); + } + + // Substring must be entirely within the same text node. + int32_t startContentOffset, endContentOffset; + nsresult rv = RenderedToContentOffset(aFrame, aStartRenderedOffset, + &startContentOffset); + NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); + rv = RenderedToContentOffset(aFrame, aEndRenderedOffset, &endContentOffset); + NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); + + nsIFrame* frame; + int32_t startContentOffsetInFrame; + // Get the right frame continuation -- not really a child, but a sibling of + // the primary frame passed in + rv = aFrame->GetChildFrameContainingOffset( + startContentOffset, false, &startContentOffsetInFrame, &frame); + NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); + + nsRect screenRect; + while (frame && startContentOffset < endContentOffset) { + // Start with this frame's screen rect, which we will shrink based on + // the substring we care about within it. We will then add that frame to + // the total screenRect we are returning. + nsRect frameScreenRect = frame->GetScreenRectInAppUnits(); + + // Get the length of the substring in this frame that we want the bounds for + auto [startFrameTextOffset, endFrameTextOffset] = frame->GetOffsets(); + int32_t frameTotalTextLength = endFrameTextOffset - startFrameTextOffset; + int32_t seekLength = endContentOffset - startContentOffset; + int32_t frameSubStringLength = + std::min(frameTotalTextLength - startContentOffsetInFrame, seekLength); + + // Add the point where the string starts to the frameScreenRect + nsPoint frameTextStartPoint; + rv = frame->GetPointFromOffset(startContentOffset, &frameTextStartPoint); + NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); + + // Use the point for the end offset to calculate the width + nsPoint frameTextEndPoint; + rv = frame->GetPointFromOffset(startContentOffset + frameSubStringLength, + &frameTextEndPoint); + NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); + + frameScreenRect.SetRectX( + frameScreenRect.X() + + std::min(frameTextStartPoint.x, frameTextEndPoint.x), + mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x)); + + screenRect.UnionRect(frameScreenRect, screenRect); + + // Get ready to loop back for next frame continuation + startContentOffset += frameSubStringLength; + startContentOffsetInFrame = 0; + frame = frame->GetNextContinuation(); + } + + return LayoutDeviceIntRect::FromAppUnitsToNearest( + screenRect, presContext->AppUnitsPerDevPixel()); +} + +uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode, + int32_t aNodeOffset, + bool aIsEndOffset) const { + if (!aNode) return 0; + + uint32_t offset = 0; + nsINode* findNode = nullptr; + + if (aNodeOffset == -1) { + findNode = aNode; + + } else if (aNode->IsText()) { + // For text nodes, aNodeOffset comes in as a character offset + // Text offset will be added at the end, if we find the offset in this + // hypertext We want the "skipped" offset into the text (rendered text + // without the extra whitespace) + nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame(); + NS_ENSURE_TRUE(frame, 0); + + nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset); + NS_ENSURE_SUCCESS(rv, 0); + + findNode = aNode; + + } else { + // findNode could be null if aNodeOffset == # of child nodes, which means + // one of two things: + // 1) there are no children, and the passed-in node is not mContent -- use + // parentContent for the node to find + // 2) there are no children and the passed-in node is mContent, which means + // we're an empty nsIAccessibleText + // 3) there are children and we're at the end of the children + + findNode = aNode->GetChildAt_Deprecated(aNodeOffset); + if (!findNode) { + if (aNodeOffset == 0) { + if (aNode == GetNode()) { + // Case #1: this accessible has no children and thus has empty text, + // we can only be at hypertext offset 0. + return 0; + } + + // Case #2: there are no children, we're at this node. + findNode = aNode; + } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) { + // Case #3: we're after the last child, get next node to this one. + for (nsINode* tmpNode = aNode; + !findNode && tmpNode && tmpNode != mContent; + tmpNode = tmpNode->GetParent()) { + findNode = tmpNode->GetNextSibling(); + } + } + } + } + + // Get accessible for this findNode, or if that node isn't accessible, use the + // accessible for the next DOM node which has one (based on forward depth + // first search) + LocalAccessible* descendant = nullptr; + if (findNode) { + dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode); + if (brElement && brElement->IsPaddingForEmptyEditor()) { + // This <br> is the hacky "padding <br> element" used when there is no + // text in the editor. + return 0; + } + + descendant = mDoc->GetAccessible(findNode); + if (!descendant && findNode->IsContent()) { + LocalAccessible* container = mDoc->GetContainerAccessible(findNode); + if (container) { + TreeWalker walker(container, findNode->AsContent(), + TreeWalker::eWalkContextTree); + descendant = walker.Next(); + if (!descendant) descendant = container; + } + } + } + + return TransformOffset(descendant, offset, aIsEndOffset); +} + +uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant, + uint32_t aOffset, + bool aIsEndOffset) const { + // From the descendant, go up and get the immediate child of this hypertext. + uint32_t offset = aOffset; + LocalAccessible* descendant = aDescendant; + while (descendant) { + LocalAccessible* parent = descendant->LocalParent(); + if (parent == this) return GetChildOffset(descendant) + offset; + + // This offset no longer applies because the passed-in text object is not + // a child of the hypertext. This happens when there are nested hypertexts, + // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset + // to make it relative the hypertext. + // If the end offset is not supposed to be inclusive and the original point + // is not at 0 offset then the returned offset should be after an embedded + // character the original point belongs to. + if (aIsEndOffset) { + // Similar to our special casing in FindOffset, we add handling for + // bulleted lists here because PeekOffset returns the inner text node + // for a list when it should return the list bullet. + // We manually set the offset so the error doesn't propagate up. + if (offset == 0 && parent && parent->IsHTMLListItem() && + descendant->LocalPrevSibling() && + descendant->LocalPrevSibling() == + parent->AsHTMLListItem()->Bullet()) { + offset = 0; + } else { + offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0; + } + } else { + offset = 0; + } + + descendant = parent; + } + + // If the given a11y point cannot be mapped into offset relative this + // hypertext offset then return length as fallback value. + return CharacterCount(); +} + +DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const { + // 0 offset is valid even if no children. In this case the associated editor + // is empty so return a DOM point for editor root element. + if (aOffset == 0) { + RefPtr<EditorBase> editorBase = GetEditor(); + if (editorBase) { + if (editorBase->IsEmpty()) { + return DOMPoint(editorBase->GetRoot(), 0); + } + } + } + + int32_t childIdx = GetChildIndexAtOffset(aOffset); + if (childIdx == -1) return DOMPoint(); + + LocalAccessible* child = LocalChildAt(childIdx); + int32_t innerOffset = aOffset - GetChildOffset(childIdx); + + // A text leaf case. + if (child->IsTextLeaf()) { + // The point is inside the text node. This is always true for any text leaf + // except a last child one. See assertion below. + if (aOffset < GetChildOffset(childIdx + 1)) { + nsIContent* content = child->GetContent(); + int32_t idx = 0; + if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(), + innerOffset, &idx))) { + return DOMPoint(); + } + + return DOMPoint(content, idx); + } + + // Set the DOM point right after the text node. + MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount()); + innerOffset = 1; + } + + // Case of embedded object. The point is either before or after the element. + NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!"); + nsINode* node = child->GetNode(); + nsINode* parentNode = node->GetParentNode(); + return parentNode ? DOMPoint(parentNode, + parentNode->ComputeIndexOf_Deprecated(node) + + innerOffset) + : DOMPoint(); +} + +uint32_t HyperTextAccessible::FindOffset(uint32_t aOffset, + nsDirection aDirection, + nsSelectionAmount aAmount, + EWordMovementType aWordMovementType) { + NS_ASSERTION(aDirection == eDirPrevious || aAmount != eSelectBeginLine, + "eSelectBeginLine should only be used with eDirPrevious"); + + // Find a leaf accessible frame to start with. PeekOffset wants this. + HyperTextAccessible* text = this; + LocalAccessible* child = nullptr; + int32_t innerOffset = aOffset; + + do { + int32_t childIdx = text->GetChildIndexAtOffset(innerOffset); + + // We can have an empty text leaf as our only child. Since empty text + // leaves are not accessible we then have no children, but 0 is a valid + // innerOffset. + if (childIdx == -1) { + NS_ASSERTION(innerOffset == 0 && !text->ChildCount(), "No childIdx?"); + return DOMPointToOffset(text->GetNode(), 0, aDirection == eDirNext); + } + + child = text->LocalChildAt(childIdx); + + // HTML list items may need special processing because PeekOffset doesn't + // work with list bullets. + if (text->IsHTMLListItem()) { + HTMLLIAccessible* li = text->AsHTMLListItem(); + if (child == li->Bullet()) { + // XXX: the logic is broken for multichar bullets in moving by + // char/cluster/word cases. + if (text != this) { + return aDirection == eDirPrevious ? TransformOffset(text, 0, false) + : TransformOffset(text, 1, true); + } + if (aDirection == eDirPrevious) return 0; + + uint32_t nextOffset = GetChildOffset(1); + if (nextOffset == 0) return 0; + + switch (aAmount) { + case eSelectLine: + case eSelectEndLine: + // Ask a text leaf next (if not empty) to the bullet for an offset + // since list item may be multiline. + return nextOffset < CharacterCount() + ? FindOffset(nextOffset, aDirection, aAmount, + aWordMovementType) + : nextOffset; + + default: + return nextOffset; + } + } + } + + innerOffset -= text->GetChildOffset(childIdx); + + text = child->AsHyperText(); + } while (text); + + nsIFrame* childFrame = child->GetFrame(); + if (!childFrame) { + NS_ERROR("No child frame"); + return 0; + } + + int32_t innerContentOffset = innerOffset; + if (child->IsTextLeaf()) { + NS_ASSERTION(childFrame->IsTextFrame(), "Wrong frame!"); + RenderedToContentOffset(childFrame, innerOffset, &innerContentOffset); + } + + nsIFrame* frameAtOffset = childFrame; + int32_t unusedOffsetInFrame = 0; + childFrame->GetChildFrameContainingOffset( + innerContentOffset, true, &unusedOffsetInFrame, &frameAtOffset); + + const bool kIsJumpLinesOk = true; // okay to jump lines + const bool kIsScrollViewAStop = false; // do not stop at scroll views + const bool kIsKeyboardSelect = true; // is keyboard selection + const bool kIsVisualBidi = false; // use visual order for bidi text + nsPeekOffsetStruct pos( + aAmount, aDirection, innerContentOffset, nsPoint(0, 0), kIsJumpLinesOk, + kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi, false, + nsPeekOffsetStruct::ForceEditableRegion::No, aWordMovementType, false); + nsresult rv = frameAtOffset->PeekOffset(&pos); + + // PeekOffset fails on last/first lines of the text in certain cases. + bool fallBackToSelectEndLine = false; + if (NS_FAILED(rv) && aAmount == eSelectLine) { + fallBackToSelectEndLine = aDirection == eDirNext; + pos.mAmount = fallBackToSelectEndLine ? eSelectEndLine : eSelectBeginLine; + frameAtOffset->PeekOffset(&pos); + } + if (!pos.mResultContent) { + NS_ERROR("No result content!"); + return 0; + } + + // Turn the resulting DOM point into an offset. + uint32_t hyperTextOffset = DOMPointToOffset( + pos.mResultContent, pos.mContentOffset, aDirection == eDirNext); + + if (fallBackToSelectEndLine && IsLineEndCharAt(hyperTextOffset)) { + // We used eSelectEndLine, but the caller requested eSelectLine. + // If there's a '\n' at the end of the line, eSelectEndLine will stop on + // it rather than after it. This is not what we want, since the caller + // wants the next line, not the same line. + ++hyperTextOffset; + } + + if (aDirection == eDirPrevious) { + // If we reached the end during search, this means we didn't find the DOM + // point and we're actually at the start of the paragraph + if (hyperTextOffset == CharacterCount()) return 0; + + // PeekOffset stops right before bullet so return 0 to workaround it. + if (IsHTMLListItem() && aAmount == eSelectBeginLine && + hyperTextOffset > 0) { + LocalAccessible* prevOffsetChild = GetChildAtOffset(hyperTextOffset - 1); + if (prevOffsetChild == AsHTMLListItem()->Bullet()) return 0; + } + } + + return hyperTextOffset; +} + +uint32_t HyperTextAccessible::FindWordBoundary( + uint32_t aOffset, nsDirection aDirection, + EWordMovementType aWordMovementType) { + uint32_t orig = + FindOffset(aOffset, aDirection, eSelectWord, aWordMovementType); + if (aWordMovementType != eStartWord) { + return orig; + } + if (aDirection == eDirPrevious) { + // When layout.word_select.stop_at_punctuation is true (the default), + // for a word beginning with punctuation, layout treats the punctuation + // as the start of the word when moving next. However, when moving + // previous, layout stops *after* the punctuation. We want to be + // consistent regardless of movement direction and always treat punctuation + // as the start of a word. + if (!StaticPrefs::layout_word_select_stop_at_punctuation()) { + return orig; + } + // Case 1: Example: "a @" + // If aOffset is 2 or 3, orig will be 0, but it should be 2. That is, + // previous word moved back too far. + LocalAccessible* child = GetChildAtOffset(orig); + if (child && child->IsHyperText()) { + // For a multi-word embedded object, previous word correctly goes back + // to the start of the word (the embedded object). Next word (below) + // incorrectly stops after the embedded object in this case, so return + // the already correct result. + // Example: "a x y b", where "x y" is an embedded link + // If aOffset is 4, orig will be 2, which is correct. + // If we get the next word (below), we'll end up returning 3 instead. + return orig; + } + uint32_t next = FindOffset(orig, eDirNext, eSelectWord, eStartWord); + if (next < aOffset) { + // Next word stopped on punctuation. + return next; + } + // case 2: example: "a @@b" + // If aOffset is 2, 3 or 4, orig will be 4, but it should be 2. That is, + // previous word didn't go back far enough. + if (orig == 0) { + return orig; + } + // Walk backwards by offset, getting the next word. + // In the loop, o is unsigned, so o >= 0 will always be true and won't + // prevent us from decrementing at 0. Instead, we check that o doesn't + // wrap around. + for (uint32_t o = orig - 1; o < orig; --o) { + next = FindOffset(o, eDirNext, eSelectWord, eStartWord); + if (next == orig) { + // Next word and previous word were consistent. This + // punctuation problem isn't applicable here. + break; + } + if (next < orig) { + // Next word stopped on punctuation. + return next; + } + } + } else { + // When layout.word_select.stop_at_punctuation is true (the default), + // when positioned on punctuation in the middle of a word, next word skips + // the rest of the word. However, when positioned before the punctuation, + // next word moves just after the punctuation. We want to be consistent + // regardless of starting position and always stop just after the + // punctuation. + // Next word can move too far when positioned on white space too. + // Example: "a b@c" + // If aOffset is 3, orig will be 5, but it should be 4. That is, next word + // moved too far. + if (aOffset == 0) { + return orig; + } + uint32_t prev = FindOffset(orig, eDirPrevious, eSelectWord, eStartWord); + if (prev <= aOffset) { + // orig definitely isn't too far forward. + return orig; + } + // Walk backwards by offset, getting the next word. + // In the loop, o is unsigned, so o >= 0 will always be true and won't + // prevent us from decrementing at 0. Instead, we check that o doesn't + // wrap around. + for (uint32_t o = aOffset - 1; o < aOffset; --o) { + uint32_t next = FindOffset(o, eDirNext, eSelectWord, eStartWord); + if (next > aOffset && next < orig) { + return next; + } + if (next <= aOffset) { + break; + } + } + } + return orig; +} + +uint32_t HyperTextAccessible::FindLineBoundary( + uint32_t aOffset, EWhichLineBoundary aWhichLineBoundary) { + // Note: empty last line doesn't have own frame (a previous line contains '\n' + // character instead) thus when it makes a difference we need to process this + // case separately (otherwise operations are performed on previous line). + switch (aWhichLineBoundary) { + case ePrevLineBegin: { + // Fetch a previous line and move to its start (as arrow up and home keys + // were pressed). + if (IsEmptyLastLineOffset(aOffset)) { + return FindOffset(aOffset, eDirPrevious, eSelectBeginLine); + } + + uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine); + return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine); + } + + case ePrevLineEnd: { + if (IsEmptyLastLineOffset(aOffset)) return aOffset - 1; + + // If offset is at first line then return 0 (first line start). + uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectBeginLine); + if (tmpOffset == 0) return 0; + + // Otherwise move to end of previous line (as arrow up and end keys were + // pressed). + tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine); + return FindOffset(tmpOffset, eDirNext, eSelectEndLine); + } + + case eThisLineBegin: { + if (IsEmptyLastLineOffset(aOffset)) return aOffset; + + // Move to begin of the current line (as home key was pressed). + uint32_t thisLineBeginOffset = + FindOffset(aOffset, eDirPrevious, eSelectBeginLine); + if (IsCharAt(thisLineBeginOffset, kEmbeddedObjectChar)) { + // We landed on an embedded character, don't mess with possible embedded + // line breaks, and assume the offset is correct. + return thisLineBeginOffset; + } + + // Sometimes, there is the possibility layout returned an + // offset smaller than it should. Sanity-check by moving to the end of the + // previous line and see if that has a greater offset. + uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine); + tmpOffset = FindOffset(tmpOffset, eDirNext, eSelectEndLine); + if (tmpOffset > thisLineBeginOffset && tmpOffset < aOffset) { + // We found a previous line offset. Return the next character after it + // as our start offset if it points to a line end char. + return IsLineEndCharAt(tmpOffset) ? tmpOffset + 1 : tmpOffset; + } + return thisLineBeginOffset; + } + + case eThisLineEnd: + if (IsEmptyLastLineOffset(aOffset)) return aOffset; + + // Move to end of the current line (as end key was pressed). + return FindOffset(aOffset, eDirNext, eSelectEndLine); + + case eNextLineBegin: { + if (IsEmptyLastLineOffset(aOffset)) return aOffset; + + // Move to begin of the next line if any (arrow down and home keys), + // otherwise end of the current line (arrow down only). + uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine); + uint32_t characterCount = CharacterCount(); + if (tmpOffset == characterCount) { + return tmpOffset; + } + + // Now, simulate the Home key on the next line to get its real offset. + uint32_t nextLineBeginOffset = + FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine); + // Sometimes, there are line breaks inside embedded characters. If this + // is the case, the cursor is after the line break, but the offset will + // be that of the embedded character, which points to before the line + // break. We definitely want the line break included. + if (IsCharAt(nextLineBeginOffset, kEmbeddedObjectChar)) { + // We can determine if there is a line break by pressing End from + // the queried offset. If there is a line break, the offset will be 1 + // greater, since this line ends with the embed. If there is not, the + // value will be different even if a line break follows right after the + // embed. + uint32_t thisLineEndOffset = + FindOffset(aOffset, eDirNext, eSelectEndLine); + if (thisLineEndOffset == nextLineBeginOffset + 1) { + // If we're querying the offset of the embedded character, we want + // the end offset of the parent line instead. Press End + // once more from the current position, which is after the embed. + if (nextLineBeginOffset == aOffset) { + uint32_t thisLineEndOffset2 = + FindOffset(thisLineEndOffset, eDirNext, eSelectEndLine); + // The above returns an offset exclusive the final line break, so we + // need to add 1 to it to return an inclusive end offset. Make sure + // we don't overshoot if we've started from another embedded + // character that has a line break, or landed on another embedded + // character, or if the result is the very end. + return (thisLineEndOffset2 == characterCount || + (IsCharAt(thisLineEndOffset, kEmbeddedObjectChar) && + thisLineEndOffset2 == thisLineEndOffset + 1) || + IsCharAt(thisLineEndOffset2, kEmbeddedObjectChar)) + ? thisLineEndOffset2 + : thisLineEndOffset2 + 1; + } + + return thisLineEndOffset; + } + return nextLineBeginOffset; + } + + // If the resulting offset is not greater than the offset we started from, + // layout could not find the offset for us. This can happen with certain + // inline-block elements. + if (nextLineBeginOffset <= aOffset) { + // Walk forward from the offset we started from up to tmpOffset, + // stopping after a line end character. + nextLineBeginOffset = aOffset; + while (nextLineBeginOffset < tmpOffset) { + if (IsLineEndCharAt(nextLineBeginOffset)) { + return nextLineBeginOffset + 1; + } + nextLineBeginOffset++; + } + } + + return nextLineBeginOffset; + } + + case eNextLineEnd: { + if (IsEmptyLastLineOffset(aOffset)) return aOffset; + + // Move to next line end (as down arrow and end key were pressed). + uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine); + if (tmpOffset == CharacterCount()) return tmpOffset; + + return FindOffset(tmpOffset, eDirNext, eSelectEndLine); + } + } + + return 0; +} + +int32_t HyperTextAccessible::FindParagraphStartOffset(uint32_t aOffset) { + // Because layout often gives us offsets that are incompatible with + // accessibility API requirements, for example when a paragraph contains + // presentational line breaks as found in Google Docs, use the accessibility + // tree to find the start offset instead. + LocalAccessible* child = GetChildAtOffset(aOffset); + if (!child) { + return -1; // Invalid offset + } + + // Use the pivot class to search for the start offset. + Pivot p = Pivot(this); + ParagraphBoundaryRule boundaryRule = ParagraphBoundaryRule( + child, child->IsTextLeaf() ? aOffset - GetChildOffset(child) : 0, + eDirPrevious); + Accessible* match = p.Prev(child, boundaryRule, true); + if (!match || match->AsLocal() == this) { + // Found nothing, or pivot found the root of the search, startOffset is 0. + // This will include all relevant text nodes. + return 0; + } + + if (match == child) { + // We started out on a boundary. + if (match->Role() == roles::WHITESPACE) { + // We are on a line break boundary, so force pivot to find the previous + // boundary. What we want is any text before this, if any. + match = p.Prev(match, boundaryRule); + if (!match || match->AsLocal() == this) { + // Same as before, we landed on the root, so offset is definitely 0. + return 0; + } + } else if (!match->AsLocal()->IsTextLeaf()) { + // The match is a block element, which is always a starting point, so + // just return its offset. + return TransformOffset(match->AsLocal(), 0, false); + } + } + + if (match->AsLocal()->IsTextLeaf()) { + // ParagraphBoundaryRule only returns a text leaf if it contains a line + // break. We want to stop after that. + return TransformOffset(match->AsLocal(), + boundaryRule.GetLastMatchTextOffset() + 1, false); + } + + // This is a previous boundary, we don't want to include it itself. + // So, walk forward one accessible, excluding the descendants of this + // boundary if it is a block element. The below call to Next should always be + // initialized with a boundary. + SkipParagraphBoundaryRule goForwardOneRule = SkipParagraphBoundaryRule(match); + match = p.Next(match, goForwardOneRule); + // We already know that the search skipped over at least one accessible, + // so match can't be null. Get its transformed offset. + MOZ_ASSERT(match); + return TransformOffset(match->AsLocal(), 0, false); +} + +int32_t HyperTextAccessible::FindParagraphEndOffset(uint32_t aOffset) { + // Because layout often gives us offsets that are incompatible with + // accessibility API requirements, for example when a paragraph contains + // presentational line breaks as found in Google Docs, use the accessibility + // tree to find the end offset instead. + LocalAccessible* child = GetChildAtOffset(aOffset); + if (!child) { + return -1; // invalid offset + } + + // Use the pivot class to search for the end offset. + Pivot p = Pivot(this); + ParagraphBoundaryRule boundaryRule = ParagraphBoundaryRule( + child, child->IsTextLeaf() ? aOffset - GetChildOffset(child) : 0, + eDirNext, + // In order to encompass all paragraphs inside embedded objects, not just + // the first, we want to skip the anchor's subtree. + /* aSkipAnchorSubtree */ true); + // Search forward for the end offset, including child. We don't want + // to go beyond this point if this offset indicates a paragraph boundary. + Accessible* match = p.Next(child, boundaryRule, true); + if (match) { + // Found something of relevance, adjust end offset. + LocalAccessible* matchAcc = match->AsLocal(); + uint32_t matchOffset; + if (matchAcc->IsTextLeaf()) { + // ParagraphBoundaryRule only returns a text leaf if it contains a line + // break. + matchOffset = boundaryRule.GetLastMatchTextOffset() + 1; + } else if (matchAcc->Role() != roles::WHITESPACE && matchAcc != child) { + // We found a block boundary that wasn't our origin. We want to stop + // right on it, not after it, since we don't want to include the content + // of the block. + matchOffset = 0; + } else { + matchOffset = nsAccUtils::TextLength(matchAcc); + } + return TransformOffset(matchAcc, matchOffset, true); + } + + // Didn't find anything, end offset is character count. + return CharacterCount(); +} + +void HyperTextAccessible::TextBeforeOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, + int32_t* aEndOffset, + nsAString& aText) { + if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // This isn't strictly related to caching, but this new text implementation + // is being developed to make caching feasible. We put it behind this pref + // to make it easy to test while it's still under development. + return HyperTextAccessibleBase::TextBeforeOffset( + aOffset, aBoundaryType, aStartOffset, aEndOffset, aText); + } + + *aStartOffset = *aEndOffset = 0; + aText.Truncate(); + + if (aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) { + // Not supported, bail out with empty text. + return; + } + + index_t convertedOffset = ConvertMagicOffset(aOffset); + if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) { + NS_ERROR("Wrong in offset!"); + return; + } + + uint32_t adjustedOffset = convertedOffset; + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + adjustedOffset = AdjustCaretOffset(adjustedOffset); + } + + switch (aBoundaryType) { + case nsIAccessibleText::BOUNDARY_CHAR: + if (convertedOffset != 0) { + CharAt(convertedOffset - 1, aText, aStartOffset, aEndOffset); + } + break; + + case nsIAccessibleText::BOUNDARY_WORD_START: { + // If the offset is a word start (except text length offset) then move + // backward to find a start offset (end offset is the given offset). + // Otherwise move backward twice to find both start and end offsets. + if (adjustedOffset == CharacterCount()) { + *aEndOffset = + FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord); + *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord); + } else { + *aStartOffset = + FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord); + *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord); + if (*aEndOffset != static_cast<int32_t>(adjustedOffset)) { + *aEndOffset = *aStartOffset; + *aStartOffset = + FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord); + } + } + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + } + + case nsIAccessibleText::BOUNDARY_WORD_END: { + // Move word backward twice to find start and end offsets. + *aEndOffset = FindWordBoundary(convertedOffset, eDirPrevious, eEndWord); + *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + } + + case nsIAccessibleText::BOUNDARY_LINE_START: + *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineBegin); + *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineBegin); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + + case nsIAccessibleText::BOUNDARY_LINE_END: { + *aEndOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd); + int32_t tmpOffset = *aEndOffset; + // Adjust offset if line is wrapped. + if (*aEndOffset != 0 && !IsLineEndCharAt(*aEndOffset)) tmpOffset--; + + *aStartOffset = FindLineBoundary(tmpOffset, ePrevLineEnd); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + } + } +} + +void HyperTextAccessible::TextAtOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, + int32_t* aEndOffset, nsAString& aText) { + if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // This isn't strictly related to caching, but this new text implementation + // is being developed to make caching feasible. We put it behind this pref + // to make it easy to test while it's still under development. + return HyperTextAccessibleBase::TextAtOffset( + aOffset, aBoundaryType, aStartOffset, aEndOffset, aText); + } + + *aStartOffset = *aEndOffset = 0; + aText.Truncate(); + + uint32_t adjustedOffset = ConvertMagicOffset(aOffset); + if (adjustedOffset == std::numeric_limits<uint32_t>::max()) { + NS_ERROR("Wrong given offset!"); + return; + } + + switch (aBoundaryType) { + case nsIAccessibleText::BOUNDARY_CHAR: + // Return no char if caret is at the end of wrapped line (case of no line + // end character). Returning a next line char is confusing for AT. + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET && + IsCaretAtEndOfLine()) { + *aStartOffset = *aEndOffset = adjustedOffset; + } else { + CharAt(adjustedOffset, aText, aStartOffset, aEndOffset); + } + break; + + case nsIAccessibleText::BOUNDARY_WORD_START: + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + adjustedOffset = AdjustCaretOffset(adjustedOffset); + } + + *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord); + *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + + case nsIAccessibleText::BOUNDARY_WORD_END: + // Ignore the spec and follow what WebKitGtk does because Orca expects it, + // i.e. return a next word at word end offset of the current word + // (WebKitGtk behavior) instead the current word (AKT spec). + *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eEndWord); + *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + + case nsIAccessibleText::BOUNDARY_LINE_START: + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + adjustedOffset = AdjustCaretOffset(adjustedOffset); + } + + *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineBegin); + *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineBegin); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + + case nsIAccessibleText::BOUNDARY_LINE_END: + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + adjustedOffset = AdjustCaretOffset(adjustedOffset); + } + + // In contrast to word end boundary we follow the spec here. + *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd); + *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineEnd); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + + case nsIAccessibleText::BOUNDARY_PARAGRAPH: { + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + adjustedOffset = AdjustCaretOffset(adjustedOffset); + } + + if (IsEmptyLastLineOffset(adjustedOffset)) { + // We are on the last line of a paragraph where there is no text. + // For example, in a textarea where a new line has just been inserted. + // In this case, return offsets for an empty line without text content. + *aStartOffset = *aEndOffset = adjustedOffset; + break; + } + + *aStartOffset = FindParagraphStartOffset(adjustedOffset); + *aEndOffset = FindParagraphEndOffset(adjustedOffset); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + } + } +} + +void HyperTextAccessible::TextAfterOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, + int32_t* aEndOffset, + nsAString& aText) { + if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // This isn't strictly related to caching, but this new text implementation + // is being developed to make caching feasible. We put it behind this pref + // to make it easy to test while it's still under development. + return HyperTextAccessibleBase::TextAfterOffset( + aOffset, aBoundaryType, aStartOffset, aEndOffset, aText); + } + + *aStartOffset = *aEndOffset = 0; + aText.Truncate(); + + if (aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) { + // Not supported, bail out with empty text. + return; + } + + index_t convertedOffset = ConvertMagicOffset(aOffset); + if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) { + NS_ERROR("Wrong in offset!"); + return; + } + + uint32_t adjustedOffset = convertedOffset; + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + adjustedOffset = AdjustCaretOffset(adjustedOffset); + } + + switch (aBoundaryType) { + case nsIAccessibleText::BOUNDARY_CHAR: + // If caret is at the end of wrapped line (case of no line end character) + // then char after the offset is a first char at next line. + if (adjustedOffset >= CharacterCount()) { + *aStartOffset = *aEndOffset = CharacterCount(); + } else { + CharAt(adjustedOffset + 1, aText, aStartOffset, aEndOffset); + } + break; + + case nsIAccessibleText::BOUNDARY_WORD_START: + // Move word forward twice to find start and end offsets. + *aStartOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord); + *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + + case nsIAccessibleText::BOUNDARY_WORD_END: + // If the offset is a word end (except 0 offset) then move forward to find + // end offset (start offset is the given offset). Otherwise move forward + // twice to find both start and end offsets. + if (convertedOffset == 0) { + *aStartOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord); + *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord); + } else { + *aEndOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord); + *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord); + if (*aStartOffset != static_cast<int32_t>(convertedOffset)) { + *aStartOffset = *aEndOffset; + *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord); + } + } + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + + case nsIAccessibleText::BOUNDARY_LINE_START: + *aStartOffset = FindLineBoundary(adjustedOffset, eNextLineBegin); + *aEndOffset = FindLineBoundary(*aStartOffset, eNextLineBegin); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + + case nsIAccessibleText::BOUNDARY_LINE_END: + *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineEnd); + *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineEnd); + TextSubstring(*aStartOffset, *aEndOffset, aText); + break; + } +} + +already_AddRefed<AccAttributes> HyperTextAccessible::TextAttributes( + bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset, + int32_t* aEndOffset) { + if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // This isn't strictly related to caching, but this new text implementation + // is being developed to make caching feasible. We put it behind this pref + // to make it easy to test while it's still under development. + return HyperTextAccessibleBase::TextAttributes(aIncludeDefAttrs, aOffset, + aStartOffset, aEndOffset); + } + + // 1. Get each attribute and its ranges one after another. + // 2. As we get each new attribute, we pass the current start and end offsets + // as in/out parameters. In other words, as attributes are collected, + // the attribute range itself can only stay the same or get smaller. + + RefPtr<AccAttributes> attributes = new AccAttributes(); + *aStartOffset = *aEndOffset = 0; + index_t offset = ConvertMagicOffset(aOffset); + if (!offset.IsValid() || offset > CharacterCount()) { + NS_ERROR("Wrong in offset!"); + return attributes.forget(); + } + + LocalAccessible* accAtOffset = GetChildAtOffset(offset); + if (!accAtOffset) { + // Offset 0 is correct offset when accessible has empty text. Include + // default attributes if they were requested, otherwise return empty set. + if (offset == 0) { + if (aIncludeDefAttrs) { + TextAttrsMgr textAttrsMgr(this); + textAttrsMgr.GetAttributes(attributes); + } + } + return attributes.forget(); + } + + int32_t accAtOffsetIdx = accAtOffset->IndexInParent(); + uint32_t startOffset = GetChildOffset(accAtOffsetIdx); + uint32_t endOffset = GetChildOffset(accAtOffsetIdx + 1); + int32_t offsetInAcc = offset - startOffset; + + TextAttrsMgr textAttrsMgr(this, aIncludeDefAttrs, accAtOffset, + accAtOffsetIdx); + textAttrsMgr.GetAttributes(attributes, &startOffset, &endOffset); + + // Compute spelling attributes on text accessible only. + nsIFrame* offsetFrame = accAtOffset->GetFrame(); + if (offsetFrame && offsetFrame->IsTextFrame()) { + int32_t nodeOffset = 0; + RenderedToContentOffset(offsetFrame, offsetInAcc, &nodeOffset); + + // Set 'misspelled' text attribute. + // FYI: Max length of text in a text node is less than INT32_MAX (see + // NS_MAX_TEXT_FRAGMENT_LENGTH) so that nodeOffset should always + // be 0 or greater. + MOZ_DIAGNOSTIC_ASSERT(accAtOffset->GetNode()->IsText()); + MOZ_DIAGNOSTIC_ASSERT(nodeOffset >= 0); + GetSpellTextAttr(accAtOffset->GetNode(), static_cast<uint32_t>(nodeOffset), + &startOffset, &endOffset, attributes); + } + + *aStartOffset = startOffset; + *aEndOffset = endOffset; + return attributes.forget(); +} + +already_AddRefed<AccAttributes> HyperTextAccessible::DefaultTextAttributes() { + RefPtr<AccAttributes> attributes = new AccAttributes(); + + TextAttrsMgr textAttrsMgr(this); + textAttrsMgr.GetAttributes(attributes); + return attributes.forget(); +} + +void HyperTextAccessible::SetMathMLXMLRoles(AccAttributes* aAttributes) { + // Add MathML xmlroles based on the position inside the parent. + LocalAccessible* parent = LocalParent(); + if (parent) { + switch (parent->Role()) { + case roles::MATHML_CELL: + case roles::MATHML_ENCLOSED: + case roles::MATHML_ERROR: + case roles::MATHML_MATH: + case roles::MATHML_ROW: + case roles::MATHML_SQUARE_ROOT: + case roles::MATHML_STYLE: + if (Role() == roles::MATHML_OPERATOR) { + // This is an operator inside an <mrow> (or an inferred <mrow>). + // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow + // XXX We should probably do something similar for MATHML_FENCED, but + // operators do not appear in the accessible tree. See bug 1175747. + nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame()); + if (mathMLFrame) { + nsEmbellishData embellishData; + mathMLFrame->GetEmbellishData(embellishData); + if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) { + if (!LocalPrevSibling()) { + aAttributes->SetAttribute(nsGkAtoms::xmlroles, + nsGkAtoms::open_fence); + } else if (!LocalNextSibling()) { + aAttributes->SetAttribute(nsGkAtoms::xmlroles, + nsGkAtoms::close_fence); + } + } + if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) { + aAttributes->SetAttribute(nsGkAtoms::xmlroles, + nsGkAtoms::separator_); + } + } + } + break; + case roles::MATHML_FRACTION: + aAttributes->SetAttribute( + nsGkAtoms::xmlroles, IndexInParent() == 0 ? nsGkAtoms::numerator + : nsGkAtoms::denominator); + break; + case roles::MATHML_ROOT: + aAttributes->SetAttribute( + nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index); + break; + case roles::MATHML_SUB: + aAttributes->SetAttribute( + nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript); + break; + case roles::MATHML_SUP: + aAttributes->SetAttribute( + nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript); + break; + case roles::MATHML_SUB_SUP: { + int32_t index = IndexInParent(); + aAttributes->SetAttribute( + nsGkAtoms::xmlroles, + index == 0 + ? nsGkAtoms::base + : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript)); + } break; + case roles::MATHML_UNDER: + aAttributes->SetAttribute( + nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript); + break; + case roles::MATHML_OVER: + aAttributes->SetAttribute( + nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript); + break; + case roles::MATHML_UNDER_OVER: { + int32_t index = IndexInParent(); + aAttributes->SetAttribute(nsGkAtoms::xmlroles, + index == 0 + ? nsGkAtoms::base + : (index == 1 ? nsGkAtoms::underscript + : nsGkAtoms::overscript)); + } break; + case roles::MATHML_MULTISCRIPTS: { + // Get the <multiscripts> base. + nsIContent* child; + bool baseFound = false; + for (child = parent->GetContent()->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsMathMLElement()) { + baseFound = true; + break; + } + } + if (baseFound) { + nsIContent* content = GetContent(); + if (child == content) { + // We are the base. + aAttributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::base); + } else { + // Browse the list of scripts to find us and determine our type. + bool postscript = true; + bool subscript = true; + for (child = child->GetNextSibling(); child; + child = child->GetNextSibling()) { + if (!child->IsMathMLElement()) continue; + if (child->IsMathMLElement(nsGkAtoms::mprescripts_)) { + postscript = false; + subscript = true; + continue; + } + if (child == content) { + if (postscript) { + aAttributes->SetAttribute(nsGkAtoms::xmlroles, + subscript ? nsGkAtoms::subscript + : nsGkAtoms::superscript); + } else { + aAttributes->SetAttribute(nsGkAtoms::xmlroles, + subscript + ? nsGkAtoms::presubscript + : nsGkAtoms::presuperscript); + } + break; + } + subscript = !subscript; + } + } + } + } break; + default: + break; + } + } +} + +already_AddRefed<AccAttributes> HyperTextAccessible::NativeAttributes() { + RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes(); + + // 'formatting' attribute is deprecated, 'display' attribute should be + // instead. + nsIFrame* frame = GetFrame(); + if (frame && frame->IsBlockFrame()) { + attributes->SetAttribute(nsGkAtoms::formatting, nsGkAtoms::block); + } + + if (FocusMgr()->IsFocused(this)) { + int32_t lineNumber = CaretLineNumber(); + if (lineNumber >= 1) { + attributes->SetAttribute(nsGkAtoms::lineNumber, lineNumber); + } + } + + if (HasOwnContent()) { + GetAccService()->MarkupAttributes(this, attributes); + if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes); + } + + return attributes.forget(); +} + +int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY, + uint32_t aCoordType) { + nsIFrame* hyperFrame = GetFrame(); + if (!hyperFrame) return -1; + + LayoutDeviceIntPoint coords = + nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this); + + nsPresContext* presContext = mDoc->PresContext(); + nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits( + coords, presContext->AppUnitsPerDevPixel()); + + nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits(); + if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) { + return -1; // Not found + } + + nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(), + coordsInAppUnits.y - frameScreenRect.Y()); + + // Go through the frames to check if each one has the point. + // When one does, add up the character offsets until we have a match + + // We have an point in an accessible child of this, now we need to add up the + // offsets before it to what we already have + int32_t offset = 0; + uint32_t childCount = ChildCount(); + for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { + LocalAccessible* childAcc = mChildren[childIdx]; + + nsIFrame* primaryFrame = childAcc->GetFrame(); + NS_ENSURE_TRUE(primaryFrame, -1); + + nsIFrame* frame = primaryFrame; + while (frame) { + nsIContent* content = frame->GetContent(); + NS_ENSURE_TRUE(content, -1); + nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame); + nsSize frameSize = frame->GetSize(); + if (pointInFrame.x < frameSize.width && + pointInFrame.y < frameSize.height) { + // Finished + if (frame->IsTextFrame()) { + nsIFrame::ContentOffsets contentOffsets = + frame->GetContentOffsetsFromPointExternal( + pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE); + if (contentOffsets.IsNull() || contentOffsets.content != content) { + return -1; // Not found + } + uint32_t addToOffset; + nsresult rv = ContentToRenderedOffset( + primaryFrame, contentOffsets.offset, &addToOffset); + NS_ENSURE_SUCCESS(rv, -1); + offset += addToOffset; + } + return offset; + } + frame = frame->GetNextContinuation(); + } + + offset += nsAccUtils::TextLength(childAcc); + } + + return -1; // Not found +} + +LayoutDeviceIntRect HyperTextAccessible::TextBounds(int32_t aStartOffset, + int32_t aEndOffset, + uint32_t aCoordType) { + index_t startOffset = ConvertMagicOffset(aStartOffset); + index_t endOffset = ConvertMagicOffset(aEndOffset); + if (!startOffset.IsValid() || !endOffset.IsValid() || + startOffset > endOffset || endOffset > CharacterCount()) { + NS_ERROR("Wrong in offset"); + return LayoutDeviceIntRect(); + } + + if (CharacterCount() == 0) { + nsPresContext* presContext = mDoc->PresContext(); + // Empty content, use our own bound to at least get x,y coordinates + nsIFrame* frame = GetFrame(); + if (!frame) { + return LayoutDeviceIntRect(); + } + return LayoutDeviceIntRect::FromAppUnitsToNearest( + frame->GetScreenRectInAppUnits(), presContext->AppUnitsPerDevPixel()); + } + + int32_t childIdx = GetChildIndexAtOffset(startOffset); + if (childIdx == -1) return LayoutDeviceIntRect(); + + LayoutDeviceIntRect bounds; + int32_t prevOffset = GetChildOffset(childIdx); + int32_t offset1 = startOffset - prevOffset; + + while (childIdx < static_cast<int32_t>(ChildCount())) { + nsIFrame* frame = LocalChildAt(childIdx++)->GetFrame(); + if (!frame) { + MOZ_ASSERT_UNREACHABLE("No frame for a child!"); + continue; + } + + int32_t nextOffset = GetChildOffset(childIdx); + if (nextOffset >= static_cast<int32_t>(endOffset)) { + bounds.UnionRect( + bounds, GetBoundsInFrame(frame, offset1, endOffset - prevOffset)); + break; + } + + bounds.UnionRect(bounds, + GetBoundsInFrame(frame, offset1, nextOffset - prevOffset)); + + prevOffset = nextOffset; + offset1 = 0; + } + + // 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 = mDoc->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()); + + auto boundsX = bounds.X(); + auto boundsY = bounds.Y(); + nsAccUtils::ConvertScreenCoordsTo(&boundsX, &boundsY, aCoordType, this); + bounds.MoveTo(boundsX, boundsY); + return bounds; +} + +already_AddRefed<EditorBase> HyperTextAccessible::GetEditor() const { + if (!mContent->HasFlag(NODE_IS_EDITABLE)) { + // If we're inside an editable container, then return that container's + // editor + LocalAccessible* ancestor = LocalParent(); + while (ancestor) { + HyperTextAccessible* hyperText = ancestor->AsHyperText(); + if (hyperText) { + // Recursion will stop at container doc because it has its own impl + // of GetEditor() + return hyperText->GetEditor(); + } + + ancestor = ancestor->LocalParent(); + } + + return nullptr; + } + + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent); + nsCOMPtr<nsIEditingSession> editingSession; + docShell->GetEditingSession(getter_AddRefs(editingSession)); + if (!editingSession) return nullptr; // No editing session interface + + dom::Document* docNode = mDoc->DocumentNode(); + RefPtr<HTMLEditor> htmlEditor = + editingSession->GetHTMLEditorForWindow(docNode->GetWindow()); + return htmlEditor.forget(); +} + +/** + * =================== Caret & Selection ====================== + */ + +nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos, + int32_t aEndPos) { + // Before setting the selection range, we need to ensure that the editor + // is initialized. (See bug 804927.) + // Otherwise, it's possible that lazy editor initialization will override + // the selection we set here and leave the caret at the end of the text. + // By calling GetEditor here, we ensure that editor initialization is + // completed before we set the selection. + RefPtr<EditorBase> editorBase = GetEditor(); + + bool isFocusable = InteractiveState() & states::FOCUSABLE; + + // If accessible is focusable then focus it before setting the selection to + // neglect control's selection changes on focus if any (for example, inputs + // that do select all on focus). + // some input controls + if (isFocusable) TakeFocus(); + + RefPtr<dom::Selection> domSel = DOMSelection(); + NS_ENSURE_STATE(domSel); + + // Set up the selection. + for (const uint32_t idx : Reversed(IntegerRange(1u, domSel->RangeCount()))) { + MOZ_ASSERT(domSel->RangeCount() == idx + 1); + RefPtr<nsRange> range{domSel->GetRangeAt(idx)}; + if (!range) { + break; // The range count has been changed by somebody else. + } + domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, + IgnoreErrors()); + } + SetSelectionBoundsAt(0, aStartPos, aEndPos); + + // Make sure it is visible + domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, + ScrollAxis(), ScrollAxis(), + dom::Selection::SCROLL_FOR_CARET_MOVE | + dom::Selection::SCROLL_OVERFLOW_HIDDEN); + + // When selection is done, move the focus to the selection if accessible is + // not focusable. That happens when selection is set within hypertext + // accessible. + if (isFocusable) return NS_OK; + + nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager(); + if (DOMFocusManager) { + NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE); + dom::Document* docNode = mDoc->DocumentNode(); + NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE); + nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow(); + RefPtr<dom::Element> result; + DOMFocusManager->MoveFocus( + window, nullptr, nsIFocusManager::MOVEFOCUS_CARET, + nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result)); + } + + return NS_OK; +} + +int32_t HyperTextAccessible::CaretOffset() const { + // Not focused focusable accessible except document accessible doesn't have + // a caret. + if (!IsDoc() && !FocusMgr()->IsFocused(this) && + (InteractiveState() & states::FOCUSABLE)) { + return -1; + } + + // Check cached value. + int32_t caretOffset = -1; + HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset); + + // Use cached value if it corresponds to this accessible. + if (caretOffset != -1) { + if (text == this) return caretOffset; + + nsINode* textNode = text->GetNode(); + // Ignore offset if cached accessible isn't a text leaf. + if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) { + return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false); + } + } + + // No caret if the focused node is not inside this DOM node and this DOM node + // is not inside of focused node. + FocusManager::FocusDisposition focusDisp = + FocusMgr()->IsInOrContainsFocus(this); + if (focusDisp == FocusManager::eNone) return -1; + + // Turn the focus node and offset of the selection into caret hypretext + // offset. + dom::Selection* domSel = DOMSelection(); + NS_ENSURE_TRUE(domSel, -1); + + nsINode* focusNode = domSel->GetFocusNode(); + uint32_t focusOffset = domSel->FocusOffset(); + + // No caret if this DOM node is inside of focused node but the selection's + // focus point is not inside of this DOM node. + if (focusDisp == FocusManager::eContainedByFocus) { + nsINode* resultNode = + nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset); + + nsINode* thisNode = GetNode(); + if (resultNode != thisNode && + !nsCoreUtils::IsAncestorOf(thisNode, resultNode)) { + return -1; + } + } + + return DOMPointToOffset(focusNode, focusOffset); +} + +int32_t HyperTextAccessible::CaretLineNumber() { + // Provide the line number for the caret, relative to the + // currently focused node. Use a 1-based index + RefPtr<nsFrameSelection> frameSelection = FrameSelection(); + if (!frameSelection) return -1; + + dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal); + if (!domSel) return -1; + + nsINode* caretNode = domSel->GetFocusNode(); + if (!caretNode || !caretNode->IsContent()) return -1; + + nsIContent* caretContent = caretNode->AsContent(); + if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1; + + int32_t returnOffsetUnused; + uint32_t caretOffset = domSel->FocusOffset(); + CaretAssociationHint hint = frameSelection->GetHint(); + nsIFrame* caretFrame = frameSelection->GetFrameForNodeOffset( + caretContent, caretOffset, hint, &returnOffsetUnused); + NS_ENSURE_TRUE(caretFrame, -1); + + AutoAssertNoDomMutations guard; // The nsILineIterators below will break if + // the DOM is modified while they're in use! + int32_t lineNumber = 1; + nsILineIterator* lineIterForCaret = nullptr; + nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr; + while (caretFrame) { + if (hyperTextContent == caretFrame->GetContent()) { + return lineNumber; // Must be in a single line hyper text, there is no + // line iterator + } + nsContainerFrame* parentFrame = caretFrame->GetParent(); + if (!parentFrame) break; + + // Add lines for the sibling frames before the caret + nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild(); + while (sibling && sibling != caretFrame) { + nsILineIterator* lineIterForSibling = sibling->GetLineIterator(); + if (lineIterForSibling) { + // For the frames before that grab all the lines + int32_t addLines = lineIterForSibling->GetNumLines(); + lineNumber += addLines; + } + sibling = sibling->GetNextSibling(); + } + + // Get the line number relative to the container with lines + if (!lineIterForCaret) { // Add the caret line just once + lineIterForCaret = parentFrame->GetLineIterator(); + if (lineIterForCaret) { + // Ancestor of caret + int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame); + lineNumber += addLines; + } + } + + caretFrame = parentFrame; + } + + MOZ_ASSERT_UNREACHABLE( + "DOM ancestry had this hypertext but frame ancestry didn't"); + return lineNumber; +} + +LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) { + *aWidget = nullptr; + + RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret(); + NS_ENSURE_TRUE(caret, LayoutDeviceIntRect()); + + bool isVisible = caret->IsVisible(); + if (!isVisible) return LayoutDeviceIntRect(); + + nsRect rect; + nsIFrame* frame = caret->GetGeometry(&rect); + if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect(); + + nsPoint offset; + // Offset from widget origin to the frame origin, which includes chrome + // on the widget. + *aWidget = frame->GetNearestWidget(offset); + NS_ENSURE_TRUE(*aWidget, LayoutDeviceIntRect()); + rect.MoveBy(offset); + + LayoutDeviceIntRect caretRect = LayoutDeviceIntRect::FromUnknownRect( + rect.ToOutsidePixels(frame->PresContext()->AppUnitsPerDevPixel())); + // clang-format off + // ((content screen origin) - (content offset in the widget)) = widget origin on the screen + // clang-format on + caretRect.MoveBy((*aWidget)->WidgetToScreenOffset() - + (*aWidget)->GetClientOffset()); + + // Correct for character size, so that caret always matches the size of + // the character. This is important for font size transitions, and is + // necessary because the Gecko caret uses the previous character's size as + // the user moves forward in the text by character. + int32_t caretOffset = CaretOffset(); + if (NS_WARN_IF(caretOffset == -1)) { + // The caret offset will be -1 if this Accessible isn't focused. Note that + // the DOM node contaning the caret might be focused, but the Accessible + // might not be; e.g. due to an autocomplete popup suggestion having a11y + // focus. + return LayoutDeviceIntRect(); + } + LayoutDeviceIntRect charRect = CharBounds( + caretOffset, nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE); + if (!charRect.IsEmpty()) { + caretRect.SetTopEdge(charRect.Y()); + } + return caretRect; +} + +void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType, + nsTArray<nsRange*>* aRanges) { + // Ignore selection if it is not visible. + RefPtr<nsFrameSelection> frameSelection = FrameSelection(); + if (!frameSelection || frameSelection->GetDisplaySelection() <= + nsISelectionController::SELECTION_HIDDEN) { + return; + } + + dom::Selection* domSel = frameSelection->GetSelection(aSelectionType); + if (!domSel) return; + + nsINode* startNode = GetNode(); + + RefPtr<EditorBase> editorBase = GetEditor(); + if (editorBase) { + startNode = editorBase->GetRoot(); + } + + if (!startNode) return; + + uint32_t childCount = startNode->GetChildCount(); + nsresult rv = domSel->GetRangesForIntervalArray(startNode, 0, startNode, + childCount, true, aRanges); + NS_ENSURE_SUCCESS_VOID(rv); + + // Remove collapsed ranges + aRanges->RemoveElementsBy( + [](const auto& range) { return range->Collapsed(); }); +} + +int32_t HyperTextAccessible::SelectionCount() { + nsTArray<nsRange*> ranges; + GetSelectionDOMRanges(SelectionType::eNormal, &ranges); + return ranges.Length(); +} + +bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum, + int32_t* aStartOffset, + int32_t* aEndOffset) { + *aStartOffset = *aEndOffset = 0; + + nsTArray<nsRange*> ranges; + GetSelectionDOMRanges(SelectionType::eNormal, &ranges); + + uint32_t rangeCount = ranges.Length(); + if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount)) { + return false; + } + + nsRange* range = ranges[aSelectionNum]; + + // Get start and end points. + nsINode* startNode = range->GetStartContainer(); + nsINode* endNode = range->GetEndContainer(); + uint32_t startOffset = range->StartOffset(); + uint32_t endOffset = range->EndOffset(); + + // Make sure start is before end, by swapping DOM points. This occurs when + // the user selects backwards in the text. + const Maybe<int32_t> order = + nsContentUtils::ComparePoints(endNode, endOffset, startNode, startOffset); + + if (!order) { + MOZ_ASSERT_UNREACHABLE(); + return false; + } + + if (*order < 0) { + std::swap(startNode, endNode); + std::swap(startOffset, endOffset); + } + + if (!startNode->IsInclusiveDescendantOf(mContent)) { + *aStartOffset = 0; + } else { + *aStartOffset = + DOMPointToOffset(startNode, AssertedCast<int32_t>(startOffset)); + } + + if (!endNode->IsInclusiveDescendantOf(mContent)) { + *aEndOffset = CharacterCount(); + } else { + *aEndOffset = + DOMPointToOffset(endNode, AssertedCast<int32_t>(endOffset), true); + } + return true; +} + +bool HyperTextAccessible::SetSelectionBoundsAt(int32_t aSelectionNum, + int32_t aStartOffset, + int32_t aEndOffset) { + index_t startOffset = ConvertMagicOffset(aStartOffset); + index_t endOffset = ConvertMagicOffset(aEndOffset); + if (!startOffset.IsValid() || !endOffset.IsValid() || + std::max(startOffset, endOffset) > CharacterCount()) { + NS_ERROR("Wrong in offset"); + return false; + } + + TextRange range(this, this, startOffset, this, endOffset); + return range.SetSelectionAt(aSelectionNum); +} + +bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) { + RefPtr<dom::Selection> domSel = DOMSelection(); + if (!domSel) return false; + + if (aSelectionNum < 0 || + aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) { + return false; + } + + const RefPtr<nsRange> range{ + domSel->GetRangeAt(static_cast<uint32_t>(aSelectionNum))}; + domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, + IgnoreErrors()); + return true; +} + +void HyperTextAccessible::ScrollSubstringTo(int32_t aStartOffset, + int32_t aEndOffset, + uint32_t aScrollType) { + TextRange range(this, this, aStartOffset, this, aEndOffset); + range.ScrollIntoView(aScrollType); +} + +void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset, + int32_t aEndOffset, + uint32_t aCoordinateType, + int32_t aX, int32_t aY) { + nsIFrame* frame = GetFrame(); + if (!frame) return; + + LayoutDeviceIntPoint coords = + nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this); + + RefPtr<nsRange> domRange = nsRange::Create(mContent); + TextRange range(this, this, aStartOffset, this, aEndOffset); + if (!range.AssignDOMRange(domRange)) { + return; + } + + nsPresContext* presContext = frame->PresContext(); + nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits( + coords, presContext->AppUnitsPerDevPixel()); + + bool initialScrolled = false; + nsIFrame* parentFrame = frame; + while ((parentFrame = parentFrame->GetParent())) { + nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame); + if (scrollableFrame) { + if (!initialScrolled) { + // Scroll substring to the given point. Turn the point into percents + // relative scrollable area to use nsCoreUtils::ScrollSubstringTo. + nsRect frameRect = parentFrame->GetScreenRectInAppUnits(); + nscoord offsetPointX = coordsInAppUnits.x - frameRect.X(); + nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y(); + + nsSize size(parentFrame->GetSize()); + + // avoid divide by zero + size.width = size.width ? size.width : 1; + size.height = size.height ? size.height : 1; + + int16_t hPercent = offsetPointX * 100 / size.width; + int16_t vPercent = offsetPointY * 100 / size.height; + + nsresult rv = nsCoreUtils::ScrollSubstringTo( + frame, domRange, + ScrollAxis(WhereToScroll(vPercent), WhenToScroll::Always), + ScrollAxis(WhereToScroll(hPercent), WhenToScroll::Always)); + if (NS_FAILED(rv)) return; + + initialScrolled = true; + } else { + // Substring was scrolled to the given point already inside its closest + // scrollable area. If there are nested scrollable areas then make + // sure we scroll lower areas to the given point inside currently + // traversed scrollable area. + nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords); + } + } + frame = parentFrame; + } +} + +void HyperTextAccessible::EnclosingRange(a11y::TextRange& aRange) const { + if (IsTextField()) { + aRange.Set(mDoc, const_cast<HyperTextAccessible*>(this), 0, + const_cast<HyperTextAccessible*>(this), CharacterCount()); + } else { + aRange.Set(mDoc, mDoc, 0, mDoc, mDoc->CharacterCount()); + } +} + +void HyperTextAccessible::SelectionRanges( + nsTArray<a11y::TextRange>* aRanges) const { + dom::Selection* sel = DOMSelection(); + if (!sel) { + return; + } + + TextRange::TextRangesFromSelection(sel, aRanges); +} + +void HyperTextAccessible::VisibleRanges( + nsTArray<a11y::TextRange>* aRanges) const {} + +void HyperTextAccessible::RangeByChild(LocalAccessible* aChild, + a11y::TextRange& aRange) const { + HyperTextAccessible* ht = aChild->AsHyperText(); + if (ht) { + aRange.Set(mDoc, ht, 0, ht, ht->CharacterCount()); + return; + } + + LocalAccessible* child = aChild; + LocalAccessible* parent = nullptr; + while ((parent = child->LocalParent()) && !(ht = parent->AsHyperText())) { + child = parent; + } + + // If no text then return collapsed text range, otherwise return a range + // containing the text enclosed by the given child. + if (ht) { + int32_t childIdx = child->IndexInParent(); + int32_t startOffset = ht->GetChildOffset(childIdx); + int32_t endOffset = + child->IsTextLeaf() ? ht->GetChildOffset(childIdx + 1) : startOffset; + aRange.Set(mDoc, ht, startOffset, ht, endOffset); + } +} + +void HyperTextAccessible::RangeAtPoint(int32_t aX, int32_t aY, + a11y::TextRange& aRange) const { + LocalAccessible* child = + mDoc->LocalChildAtPoint(aX, aY, EWhichChildAtPoint::DeepestChild); + if (!child) return; + + LocalAccessible* parent = nullptr; + while ((parent = child->LocalParent()) && !parent->IsHyperText()) { + child = parent; + } + + // Return collapsed text range for the point. + if (parent) { + HyperTextAccessible* ht = parent->AsHyperText(); + int32_t offset = ht->GetChildOffset(child); + aRange.Set(mDoc, ht, offset, ht, offset); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible public + +// LocalAccessible protected +ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const { + // Check @alt attribute for invalid img elements. + if (mContent->IsHTMLElement(nsGkAtoms::img)) { + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::alt, aName); + if (!aName.IsEmpty()) return eNameOK; + } + + ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName); + if (!aName.IsEmpty()) return nameFlag; + + // Get name from title attribute for HTML abbr and acronym elements making it + // a valid name from markup. Otherwise their name isn't picked up by recursive + // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP. + if (IsAbbreviation() && mContent->AsElement()->GetAttr( + kNameSpaceID_None, nsGkAtoms::title, aName)) { + aName.CompressWhitespace(); + } + + return eNameOK; +} + +void HyperTextAccessible::Shutdown() { + mOffsets.Clear(); + AccessibleWrap::Shutdown(); +} + +bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) { + const int32_t childIndex = aAccessible->IndexInParent(); + if (childIndex < static_cast<int32_t>(mOffsets.Length())) { + mOffsets.RemoveLastElements(mOffsets.Length() - childIndex); + } + + return AccessibleWrap::RemoveChild(aAccessible); +} + +bool HyperTextAccessible::InsertChildAt(uint32_t aIndex, + LocalAccessible* aChild) { + if (aIndex < mOffsets.Length()) { + mOffsets.RemoveLastElements(mOffsets.Length() - aIndex); + } + + return AccessibleWrap::InsertChildAt(aIndex, aChild); +} + +Relation HyperTextAccessible::RelationByType(RelationType aType) const { + Relation rel = LocalAccessible::RelationByType(aType); + + switch (aType) { + case RelationType::NODE_CHILD_OF: + if (HasOwnContent() && mContent->IsMathMLElement()) { + LocalAccessible* parent = LocalParent(); + if (parent) { + nsIContent* parentContent = parent->GetContent(); + if (parentContent && + parentContent->IsMathMLElement(nsGkAtoms::mroot_)) { + // Add a relation pointing to the parent <mroot>. + rel.AppendTarget(parent); + } + } + } + break; + case RelationType::NODE_PARENT_OF: + if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot_)) { + LocalAccessible* base = LocalChildAt(0); + LocalAccessible* index = LocalChildAt(1); + if (base && index) { + // Append the <mroot> children in the order index, base. + rel.AppendTarget(index); + rel.AppendTarget(base); + } + } + break; + default: + break; + } + + return rel; +} + +//////////////////////////////////////////////////////////////////////////////// +// HyperTextAccessible public static + +nsresult HyperTextAccessible::ContentToRenderedOffset( + nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const { + if (!aFrame) { + // Current frame not rendered -- this can happen if text is set on + // something with display: none + *aRenderedOffset = 0; + return NS_OK; + } + + if (IsTextField()) { + *aRenderedOffset = aContentOffset; + return NS_OK; + } + + NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion"); + NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr, + "Call on primary frame only"); + + nsIFrame::RenderedText text = + aFrame->GetRenderedText(aContentOffset, aContentOffset + 1, + nsIFrame::TextOffsetType::OffsetsInContentText, + nsIFrame::TrailingWhitespace::DontTrim); + *aRenderedOffset = text.mOffsetWithinNodeRenderedText; + + return NS_OK; +} + +nsresult HyperTextAccessible::RenderedToContentOffset( + nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const { + if (IsTextField()) { + *aContentOffset = aRenderedOffset; + return NS_OK; + } + + *aContentOffset = 0; + NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE); + + NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion"); + NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr, + "Call on primary frame only"); + + nsIFrame::RenderedText text = + aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1, + nsIFrame::TextOffsetType::OffsetsInRenderedText, + nsIFrame::TrailingWhitespace::DontTrim); + *aContentOffset = text.mOffsetWithinNodeText; + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// HyperTextAccessible protected + +nsresult HyperTextAccessible::GetDOMPointByFrameOffset( + nsIFrame* aFrame, int32_t aOffset, LocalAccessible* aAccessible, + DOMPoint* aPoint) { + NS_ENSURE_ARG(aAccessible); + + if (!aFrame) { + // If the given frame is null then set offset after the DOM node of the + // given accessible. + NS_ASSERTION(!aAccessible->IsDoc(), + "Shouldn't be called on document accessible!"); + + nsIContent* content = aAccessible->GetContent(); + NS_ASSERTION(content, "Shouldn't operate on defunct accessible!"); + + nsIContent* parent = content->GetParent(); + + aPoint->idx = parent->ComputeIndexOf_Deprecated(content) + 1; + aPoint->node = parent; + + } else if (aFrame->IsTextFrame()) { + nsIContent* content = aFrame->GetContent(); + NS_ENSURE_STATE(content); + + nsIFrame* primaryFrame = content->GetPrimaryFrame(); + nsresult rv = + RenderedToContentOffset(primaryFrame, aOffset, &(aPoint->idx)); + NS_ENSURE_SUCCESS(rv, rv); + + aPoint->node = content; + + } else { + nsIContent* content = aFrame->GetContent(); + NS_ENSURE_STATE(content); + + nsIContent* parent = content->GetParent(); + NS_ENSURE_STATE(parent); + + aPoint->idx = parent->ComputeIndexOf_Deprecated(content); + aPoint->node = parent; + } + + return NS_OK; +} + +// HyperTextAccessible +void HyperTextAccessible::GetSpellTextAttr(nsINode* aNode, uint32_t aNodeOffset, + uint32_t* aStartOffset, + uint32_t* aEndOffset, + AccAttributes* aAttributes) { + RefPtr<nsFrameSelection> fs = FrameSelection(); + if (!fs) return; + + dom::Selection* domSel = fs->GetSelection(SelectionType::eSpellCheck); + if (!domSel) return; + + const uint32_t rangeCount = domSel->RangeCount(); + if (!rangeCount) { + return; + } + + uint32_t startOffset = 0, endOffset = 0; + for (const uint32_t idx : IntegerRange(rangeCount)) { + MOZ_ASSERT(domSel->RangeCount() == rangeCount); + const nsRange* range = domSel->GetRangeAt(idx); + MOZ_ASSERT(range); + if (range->Collapsed()) continue; + + // See if the point comes after the range in which case we must continue in + // case there is another range after this one. + nsINode* endNode = range->GetEndContainer(); + uint32_t endNodeOffset = range->EndOffset(); + Maybe<int32_t> order = nsContentUtils::ComparePoints( + aNode, aNodeOffset, endNode, endNodeOffset); + if (NS_WARN_IF(!order)) { + continue; + } + + if (*order >= 0) { + continue; + } + + // At this point our point is either in this range or before it but after + // the previous range. So we check to see if the range starts before the + // point in which case the point is in the missspelled range, otherwise it + // must be before the range and after the previous one if any. + nsINode* startNode = range->GetStartContainer(); + int32_t startNodeOffset = range->StartOffset(); + order = nsContentUtils::ComparePoints(startNode, startNodeOffset, aNode, + aNodeOffset); + if (!order) { + // As (`aNode`, `aNodeOffset`) is comparable to the end of the range, it + // should also be comparable to the range's start. Returning here + // prevents crashes in release builds. + MOZ_ASSERT_UNREACHABLE(); + return; + } + + if (*order <= 0) { + startOffset = DOMPointToOffset(startNode, startNodeOffset); + + endOffset = DOMPointToOffset(endNode, endNodeOffset); + + if (startOffset > *aStartOffset) *aStartOffset = startOffset; + + if (endOffset < *aEndOffset) *aEndOffset = endOffset; + + aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::spelling); + + return; + } + + // This range came after the point. + endOffset = DOMPointToOffset(startNode, startNodeOffset); + + if (idx > 0) { + const nsRange* prevRange = domSel->GetRangeAt(idx - 1); + startOffset = DOMPointToOffset(prevRange->GetEndContainer(), + prevRange->EndOffset()); + } + + // The previous range might not be within this accessible. In that case, + // DOMPointToOffset returns length as a fallback. We don't want to use + // that offset if so, hence the startOffset < *aEndOffset check. + if (startOffset > *aStartOffset && startOffset < *aEndOffset) { + *aStartOffset = startOffset; + } + + if (endOffset < *aEndOffset) *aEndOffset = endOffset; + + return; + } + + // We never found a range that ended after the point, therefore we know that + // the point is not in a range, that we do not need to compute an end offset, + // and that we should use the end offset of the last range to compute the + // start offset of the text attribute range. + const nsRange* prevRange = domSel->GetRangeAt(rangeCount - 1); + startOffset = + DOMPointToOffset(prevRange->GetEndContainer(), prevRange->EndOffset()); + + // The previous range might not be within this accessible. In that case, + // DOMPointToOffset returns length as a fallback. We don't want to use + // that offset if so, hence the startOffset < *aEndOffset check. + if (startOffset > *aStartOffset && startOffset < *aEndOffset) { + *aStartOffset = startOffset; + } +} diff --git a/accessible/generic/HyperTextAccessible.h b/accessible/generic/HyperTextAccessible.h new file mode 100644 index 0000000000..c0fa53fd2a --- /dev/null +++ b/accessible/generic/HyperTextAccessible.h @@ -0,0 +1,455 @@ +/* -*- 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_HyperTextAccessible_h__ +#define mozilla_a11y_HyperTextAccessible_h__ + +#include "AccessibleWrap.h" +#include "mozilla/a11y/HyperTextAccessibleBase.h" +#include "nsIAccessibleText.h" +#include "nsIAccessibleTypes.h" +#include "nsIFrame.h" // only for nsSelectionAmount +#include "nsISelectionController.h" +#include "nsDirection.h" +#include "WordMovementType.h" + +class nsFrameSelection; +class nsIFrame; +class nsRange; +class nsIWidget; + +namespace mozilla { +class EditorBase; +namespace dom { +class Selection; +} + +namespace a11y { + +class TextLeafPoint; +class TextRange; + +struct DOMPoint { + DOMPoint() : node(nullptr), idx(0) {} + DOMPoint(nsINode* aNode, int32_t aIdx) : node(aNode), idx(aIdx) {} + + nsINode* node; + int32_t idx; +}; + +/** + * Special Accessible that knows how contain both text and embedded objects + */ +class HyperTextAccessible : public AccessibleWrap, + public HyperTextAccessibleBase { + public: + HyperTextAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(HyperTextAccessible, AccessibleWrap) + + // LocalAccessible + virtual already_AddRefed<AccAttributes> NativeAttributes() override; + virtual mozilla::a11y::role NativeRole() const override; + virtual uint64_t NativeState() const override; + + virtual void Shutdown() override; + virtual bool RemoveChild(LocalAccessible* aAccessible) override; + virtual bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) override; + virtual Relation RelationByType(RelationType aType) const override; + + /** + * Return whether the associated content is editable. + */ + bool IsEditable() const; + + // HyperTextAccessible (static helper method) + + // Convert content offset to rendered text offset + nsresult ContentToRenderedOffset(nsIFrame* aFrame, int32_t aContentOffset, + uint32_t* aRenderedOffset) const; + + // Convert rendered text offset to content offset + nsresult RenderedToContentOffset(nsIFrame* aFrame, uint32_t aRenderedOffset, + int32_t* aContentOffset) const; + + ////////////////////////////////////////////////////////////////////////////// + // HyperLinkAccessible + + /** + * Return link accessible at the given index. + */ + LocalAccessible* LinkAt(uint32_t aIndex) { return EmbeddedChildAt(aIndex); } + + ////////////////////////////////////////////////////////////////////////////// + // HyperTextAccessible: DOM point to text offset conversions. + + /** + * Turn a DOM point (node and offset) into a character offset of this + * hypertext. Will look for closest match when the DOM node does not have + * an accessible object associated with it. Will return an offset for the end + * of the string if the node is not found. + * + * @param aNode [in] the node to look for + * @param aNodeOffset [in] the offset to look for + * if -1 just look directly for the node + * if >=0 and aNode is text, this represents a char + * offset if >=0 and aNode is not text, this represents a child node offset + * @param aIsEndOffset [in] if true, then this offset is not inclusive. The + * character indicated by the offset returned is at [offset - 1]. This means + * if the passed-in offset is really in a descendant, then the offset + * returned will come just after the relevant embedded object characer. If + * false, then the offset is inclusive. The character indicated by the offset + * returned is at [offset]. If the passed-in offset in inside a descendant, + * then the returned offset will be on the relevant embedded object char. + */ + uint32_t DOMPointToOffset(nsINode* aNode, int32_t aNodeOffset, + bool aIsEndOffset = false) const; + + /** + * Transform the given a11y point into the offset relative this hypertext. + */ + uint32_t TransformOffset(LocalAccessible* aDescendant, uint32_t aOffset, + bool aIsEndOffset) const; + + /** + * Convert the given offset into DOM point. + * + * If offset is at text leaf then DOM point is (text node, offsetInTextNode), + * if before embedded object then (parent node, indexInParent), if after then + * (parent node, indexInParent + 1). + */ + DOMPoint OffsetToDOMPoint(int32_t aOffset) const; + + ////////////////////////////////////////////////////////////////////////////// + // TextAccessible + + using HyperTextAccessibleBase::CharAt; + + char16_t CharAt(int32_t aOffset) { + nsAutoString charAtOffset; + CharAt(aOffset, charAtOffset); + return charAtOffset.CharAt(0); + } + + /** + * Return true if char at the given offset equals to given char. + */ + bool IsCharAt(int32_t aOffset, char16_t aChar) { + return CharAt(aOffset) == aChar; + } + + /** + * Return true if terminal char is at the given offset. + */ + bool IsLineEndCharAt(int32_t aOffset) { return IsCharAt(aOffset, '\n'); } + + virtual void TextBeforeOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText) override; + virtual void TextAtOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText) override; + virtual void TextAfterOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText) override; + + virtual already_AddRefed<AccAttributes> TextAttributes( + bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset, + int32_t* aEndOffset) override; + + virtual already_AddRefed<AccAttributes> DefaultTextAttributes() override; + + // HyperTextAccessibleBase provides an overload which takes an Accessible. + using HyperTextAccessibleBase::GetChildOffset; + + virtual LocalAccessible* GetChildAtOffset(uint32_t aOffset) const override { + return LocalChildAt(GetChildIndexAtOffset(aOffset)); + } + + /** + * Return an offset at the given point. + */ + int32_t OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType) override; + + LayoutDeviceIntRect TextBounds( + int32_t aStartOffset, int32_t aEndOffset, + uint32_t aCoordType = + nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) override; + + LayoutDeviceIntRect CharBounds(int32_t aOffset, + uint32_t aCoordType) override { + int32_t endOffset = aOffset == static_cast<int32_t>(CharacterCount()) + ? aOffset + : aOffset + 1; + return TextBounds(aOffset, endOffset, aCoordType); + } + + /** + * Get/set caret offset, if no caret then -1. + */ + virtual int32_t CaretOffset() const override; + virtual void SetCaretOffset(int32_t aOffset) override; + + /** + * Provide the line number for the caret. + * @return 1-based index for the line number with the caret + */ + int32_t CaretLineNumber(); + + /** + * Return the caret rect and the widget containing the caret within this + * text accessible. + * + * @param [out] the widget containing the caret + * @return the caret rect + */ + mozilla::LayoutDeviceIntRect GetCaretRect(nsIWidget** aWidget); + + /** + * Return true if caret is at end of line. + */ + bool IsCaretAtEndOfLine() const; + + virtual int32_t SelectionCount() override; + + virtual bool SelectionBoundsAt(int32_t aSelectionNum, int32_t* aStartOffset, + int32_t* aEndOffset) override; + + /* + * Changes the start and end offset of the specified selection. + * @return true if succeeded + */ + // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead. + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool SetSelectionBoundsAt(int32_t aSelectionNum, + int32_t aStartOffset, + int32_t aEndOffset); + + /** + * Adds a selection bounded by the specified offsets. + * @return true if succeeded + */ + bool AddToSelection(int32_t aStartOffset, int32_t aEndOffset); + + /* + * Removes the specified selection. + * @return true if succeeded + */ + // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead. + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool RemoveFromSelection(int32_t aSelectionNum); + + /** + * Scroll the given text range into view. + */ + void ScrollSubstringTo(int32_t aStartOffset, int32_t aEndOffset, + uint32_t aScrollType); + + /** + * Scroll the given text range to the given point. + */ + void ScrollSubstringToPoint(int32_t aStartOffset, int32_t aEndOffset, + uint32_t aCoordinateType, int32_t aX, int32_t aY); + + /** + * Return a range that encloses the text control or the document this + * accessible belongs to. + */ + void EnclosingRange(TextRange& aRange) const; + + virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const override; + + /** + * Return an array of disjoint ranges of visible text within the text control + * or the document this accessible belongs to. + */ + void VisibleRanges(nsTArray<TextRange>* aRanges) const; + + /** + * Return a range containing the given accessible. + */ + void RangeByChild(LocalAccessible* aChild, TextRange& aRange) const; + + /** + * Return a range containing an accessible at the given point. + */ + void RangeAtPoint(int32_t aX, int32_t aY, TextRange& aRange) const; + + ////////////////////////////////////////////////////////////////////////////// + // EditableTextAccessible + + MOZ_CAN_RUN_SCRIPT_BOUNDARY void ReplaceText(const nsAString& aText); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void InsertText(const nsAString& aText, + int32_t aPosition); + void CopyText(int32_t aStartPos, int32_t aEndPos); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void CutText(int32_t aStartPos, int32_t aEndPos); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void DeleteText(int32_t aStartPos, + int32_t aEndPos); + MOZ_CAN_RUN_SCRIPT + void PasteText(int32_t aPosition); + + /** + * Return the editor associated with the accessible. + * The result may be either TextEditor or HTMLEditor. + */ + virtual already_AddRefed<EditorBase> GetEditor() const; + + /** + * Return DOM selection object for the accessible. + */ + dom::Selection* DOMSelection() const; + + protected: + virtual ~HyperTextAccessible() {} + + // LocalAccessible + virtual ENameValueFlag NativeName(nsString& aName) const override; + + // HyperTextAccessible + + /** + * Adjust an offset the caret stays at to get a text by line boundary. + */ + uint32_t AdjustCaretOffset(uint32_t aOffset) const; + + /** + * Return true if the given offset points to terminal empty line if any. + */ + bool IsEmptyLastLineOffset(int32_t aOffset) { + return aOffset == static_cast<int32_t>(CharacterCount()) && + IsLineEndCharAt(aOffset - 1); + } + + /** + * Return an offset of the found word boundary. + */ + uint32_t FindWordBoundary(uint32_t aOffset, nsDirection aDirection, + EWordMovementType aWordMovementType); + + /** + * Used to get begin/end of previous/this/next line. Note: end of line + * is an offset right before '\n' character if any, the offset is right after + * '\n' character is begin of line. In case of wrap word breaks these offsets + * are equal. + */ + enum EWhichLineBoundary { + ePrevLineBegin, + ePrevLineEnd, + eThisLineBegin, + eThisLineEnd, + eNextLineBegin, + eNextLineEnd + }; + + /** + * Return an offset for requested line boundary. See constants above. + */ + uint32_t FindLineBoundary(uint32_t aOffset, + EWhichLineBoundary aWhichLineBoundary); + + /** + * Find the start offset for a paragraph , taking into account + * inner block elements and line breaks. + */ + int32_t FindParagraphStartOffset(uint32_t aOffset); + + /** + * Find the end offset for a paragraph , taking into account + * inner block elements and line breaks. + */ + int32_t FindParagraphEndOffset(uint32_t aOffset); + + /** + * Return an offset corresponding to the given direction and selection amount + * relative the given offset. A helper used to find word or line boundaries. + */ + uint32_t FindOffset(uint32_t aOffset, nsDirection aDirection, + nsSelectionAmount aAmount, + EWordMovementType aWordMovementType = eDefaultBehavior); + + /** + * Return the boundaries (in dev pixels) of the substring in case of textual + * frame or frame boundaries in case of non textual frame, offsets are + * ignored. + */ + LayoutDeviceIntRect GetBoundsInFrame(nsIFrame* aFrame, + uint32_t aStartRenderedOffset, + uint32_t aEndRenderedOffset); + + // Selection helpers + + /** + * Return frame selection object for the accessible. + */ + already_AddRefed<nsFrameSelection> FrameSelection() const; + + /** + * Return selection ranges within the accessible subtree. + */ + void GetSelectionDOMRanges(SelectionType aSelectionType, + nsTArray<nsRange*>* aRanges); + + // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead. + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult SetSelectionRange(int32_t aStartPos, + int32_t aEndPos); + + // Helpers + nsresult GetDOMPointByFrameOffset(nsIFrame* aFrame, int32_t aOffset, + LocalAccessible* aAccessible, + mozilla::a11y::DOMPoint* aPoint); + + /** + * Set 'misspelled' text attribute and return range offsets where the + * attibute is stretched. If the text is not misspelled at the given offset + * then we expose only range offsets where text is not misspelled. The method + * is used by TextAttributes() method. + * + * @param aIncludeDefAttrs [in] points whether text attributes having default + * values of attributes should be included + * @param aSourceNode [in] the node we start to traverse from + * @param aStartOffset [in, out] the start offset + * @param aEndOffset [in, out] the end offset + * @param aAttributes [out, optional] result attributes + */ + void GetSpellTextAttr(nsINode* aNode, uint32_t aNodeOffset, + uint32_t* aStartOffset, uint32_t* aEndOffset, + AccAttributes* aAttributes); + + /** + * Set xml-roles attributes for MathML elements. + * @param aAttributes + */ + void SetMathMLXMLRoles(AccAttributes* aAttributes); + + // HyperTextAccessibleBase + virtual const Accessible* Acc() const override { return this; } + + virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() override { + return mOffsets; + } + + private: + /** + * End text offsets array. + */ + mutable nsTArray<int32_t> mOffsets; +}; + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible downcasting method + +inline HyperTextAccessible* LocalAccessible::AsHyperText() { + return IsHyperText() ? static_cast<HyperTextAccessible*>(this) : nullptr; +} + +inline HyperTextAccessibleBase* LocalAccessible::AsHyperTextBase() { + return AsHyperText(); +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/ImageAccessible.cpp b/accessible/generic/ImageAccessible.cpp new file mode 100644 index 0000000000..d45c693654 --- /dev/null +++ b/accessible/generic/ImageAccessible.cpp @@ -0,0 +1,263 @@ +/* -*- 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 "ImageAccessible.h" + +#include "DocAccessible-inl.h" +#include "LocalAccessible-inl.h" +#include "nsAccUtils.h" +#include "Role.h" +#include "AccAttributes.h" +#include "AccIterator.h" +#include "CacheConstants.h" +#include "States.h" + +#include "imgIContainer.h" +#include "imgIRequest.h" +#include "nsGenericHTMLElement.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "nsContentUtils.h" +#include "nsIImageLoadingContent.h" +#include "nsPIDOMWindow.h" +#include "nsIURI.h" + +namespace mozilla::a11y { + +NS_IMPL_ISUPPORTS_INHERITED(ImageAccessible, LinkableAccessible, + imgINotificationObserver) + +//////////////////////////////////////////////////////////////////////////////// +// ImageAccessible +//////////////////////////////////////////////////////////////////////////////// + +ImageAccessible::ImageAccessible(nsIContent* aContent, DocAccessible* aDoc) + : LinkableAccessible(aContent, aDoc), + mImageRequestStatus(imgIRequest::STATUS_NONE) { + mType = eImageType; + nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(mContent)); + if (content) { + content->AddNativeObserver(this); + nsCOMPtr<imgIRequest> imageRequest; + content->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(imageRequest)); + if (imageRequest) { + imageRequest->GetImageStatus(&mImageRequestStatus); + } + } +} + +ImageAccessible::~ImageAccessible() {} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible public + +void ImageAccessible::Shutdown() { + nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(mContent)); + if (content) { + content->RemoveNativeObserver(this); + } + + LinkableAccessible::Shutdown(); +} + +uint64_t ImageAccessible::NativeState() const { + // The state is a bitfield, get our inherited state, then logically OR it with + // states::ANIMATED if this is an animated image. + + uint64_t state = LinkableAccessible::NativeState(); + + if (mImageRequestStatus & imgIRequest::STATUS_IS_ANIMATED) { + state |= states::ANIMATED; + } + + if (!(mImageRequestStatus & imgIRequest::STATUS_SIZE_AVAILABLE)) { + nsIFrame* frame = GetFrame(); + MOZ_ASSERT(!frame || frame->AccessibleType() == eImageType || + frame->AccessibleType() == a11y::eHTMLImageMapType || + frame->IsImageBoxFrame()); + if (frame && !(frame->GetStateBits() & IMAGE_SIZECONSTRAINED)) { + // The size of this image hasn't been constrained and we haven't loaded + // enough of the image to know its size yet. This means it currently + // has 0 width and height. + state |= states::INVISIBLE; + } + } + + return state; +} + +ENameValueFlag ImageAccessible::NativeName(nsString& aName) const { + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::alt, aName); + if (!aName.IsEmpty()) return eNameOK; + + ENameValueFlag nameFlag = LocalAccessible::NativeName(aName); + if (!aName.IsEmpty()) return nameFlag; + + return eNameOK; +} + +role ImageAccessible::NativeRole() const { return roles::GRAPHIC; } + +void ImageAccessible::DOMAttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue, + uint64_t aOldState) { + LinkableAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, + aOldValue, aOldState); + + if (aAttribute == nsGkAtoms::longdesc && + (aModType == dom::MutationEvent_Binding::ADDITION || + aModType == dom::MutationEvent_Binding::REMOVAL)) { + SendCache(CacheDomain::Actions, CacheUpdateType::Update); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +uint8_t ImageAccessible::ActionCount() const { + uint8_t actionCount = LinkableAccessible::ActionCount(); + return HasLongDesc() ? actionCount + 1 : actionCount; +} + +void ImageAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { + aName.Truncate(); + if (IsLongDescIndex(aIndex) && HasLongDesc()) { + aName.AssignLiteral("showlongdesc"); + } else { + LinkableAccessible::ActionNameAt(aIndex, aName); + } +} + +bool ImageAccessible::DoAction(uint8_t aIndex) const { + // Get the long description uri and open in a new window. + if (!IsLongDescIndex(aIndex)) return LinkableAccessible::DoAction(aIndex); + + nsCOMPtr<nsIURI> uri = GetLongDescURI(); + if (!uri) return false; + + nsAutoCString utf8spec; + uri->GetSpec(utf8spec); + NS_ConvertUTF8toUTF16 spec(utf8spec); + + dom::Document* document = mContent->OwnerDoc(); + nsCOMPtr<nsPIDOMWindowOuter> piWindow = document->GetWindow(); + if (!piWindow) return false; + + RefPtr<dom::BrowsingContext> tmp; + return NS_SUCCEEDED(piWindow->Open(spec, u""_ns, u""_ns, + /* aLoadInfo = */ nullptr, + /* aForceNoOpener = */ false, + getter_AddRefs(tmp))); +} + +//////////////////////////////////////////////////////////////////////////////// +// ImageAccessible + +LayoutDeviceIntPoint ImageAccessible::Position(uint32_t aCoordType) { + LayoutDeviceIntPoint point = Bounds().TopLeft(); + nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType, + this); + return point; +} + +LayoutDeviceIntSize ImageAccessible::Size() { return Bounds().Size(); } + +// LocalAccessible +already_AddRefed<AccAttributes> ImageAccessible::NativeAttributes() { + RefPtr<AccAttributes> attributes = LinkableAccessible::NativeAttributes(); + + nsString src; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src); + if (!src.IsEmpty()) attributes->SetAttribute(nsGkAtoms::src, std::move(src)); + + return attributes.forget(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Private methods + +already_AddRefed<nsIURI> ImageAccessible::GetLongDescURI() const { + if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::longdesc)) { + // To check if longdesc contains an invalid url. + nsAutoString longdesc; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::longdesc, + longdesc); + if (longdesc.FindChar(' ') != -1 || longdesc.FindChar('\t') != -1 || + longdesc.FindChar('\r') != -1 || longdesc.FindChar('\n') != -1) { + return nullptr; + } + nsCOMPtr<nsIURI> uri; + nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), longdesc, + mContent->OwnerDoc(), + mContent->GetBaseURI()); + return uri.forget(); + } + + DocAccessible* document = Document(); + if (document) { + IDRefsIterator iter(document, mContent, nsGkAtoms::aria_describedby); + while (nsIContent* target = iter.NextElem()) { + if ((target->IsHTMLElement(nsGkAtoms::a) || + target->IsHTMLElement(nsGkAtoms::area)) && + target->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::href)) { + nsGenericHTMLElement* element = nsGenericHTMLElement::FromNode(target); + + nsCOMPtr<nsIURI> uri; + element->GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(uri)); + return uri.forget(); + } + } + } + + return nullptr; +} + +bool ImageAccessible::IsLongDescIndex(uint8_t aIndex) const { + return aIndex == LinkableAccessible::ActionCount(); +} + +//////////////////////////////////////////////////////////////////////////////// +// imgINotificationObserver + +void ImageAccessible::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* aData) { + if (aType != imgINotificationObserver::FRAME_COMPLETE && + aType != imgINotificationObserver::LOAD_COMPLETE && + aType != imgINotificationObserver::DECODE_COMPLETE) { + // We should update our state if the whole image was decoded, + // or the first frame in the case of a gif. + return; + } + + if (IsDefunct() || !mParent) { + return; + } + + uint32_t status = imgIRequest::STATUS_NONE; + aRequest->GetImageStatus(&status); + + if ((status ^ mImageRequestStatus) & imgIRequest::STATUS_SIZE_AVAILABLE) { + nsIFrame* frame = GetFrame(); + if (frame && !(frame->GetStateBits() & IMAGE_SIZECONSTRAINED)) { + RefPtr<AccEvent> event = new AccStateChangeEvent( + this, states::INVISIBLE, + !(status & imgIRequest::STATUS_SIZE_AVAILABLE)); + mDoc->FireDelayedEvent(event); + } + } + + if ((status ^ mImageRequestStatus) & imgIRequest::STATUS_IS_ANIMATED) { + RefPtr<AccEvent> event = new AccStateChangeEvent( + this, states::ANIMATED, (status & imgIRequest::STATUS_IS_ANIMATED)); + mDoc->FireDelayedEvent(event); + } + + mImageRequestStatus = status; +} + +} // namespace mozilla::a11y diff --git a/accessible/generic/ImageAccessible.h b/accessible/generic/ImageAccessible.h new file mode 100644 index 0000000000..1a072c5f90 --- /dev/null +++ b/accessible/generic/ImageAccessible.h @@ -0,0 +1,94 @@ +/* -*- 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_ImageAccessible_h__ +#define mozilla_a11y_ImageAccessible_h__ + +#include "BaseAccessibles.h" +#include "imgINotificationObserver.h" + +namespace mozilla { +namespace a11y { + +/* LocalAccessible for supporting images + * supports: + * - gets name, role + * - support basic state + */ +class ImageAccessible : public LinkableAccessible, + public imgINotificationObserver { + public: + ImageAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_IMGINOTIFICATIONOBSERVER + + // LocalAccessible + virtual void Shutdown() override; + virtual a11y::role NativeRole() const override; + virtual uint64_t NativeState() const override; + virtual already_AddRefed<AccAttributes> NativeAttributes() override; + + // ActionAccessible + virtual uint8_t ActionCount() const override; + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + virtual bool DoAction(uint8_t aIndex) const override; + + // ImageAccessible + LayoutDeviceIntPoint Position(uint32_t aCoordType); + LayoutDeviceIntSize Size(); + + /** + * Return whether the element has a longdesc URI. + */ + bool HasLongDesc() const { + nsCOMPtr<nsIURI> uri = GetLongDescURI(); + return uri; + } + + protected: + virtual ~ImageAccessible(); + + // LocalAccessible + virtual ENameValueFlag NativeName(nsString& aName) const override; + + virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue, + uint64_t aOldState) override; + + private: + /** + * Return an URI for showlongdesc action if any. + */ + already_AddRefed<nsIURI> GetLongDescURI() const; + + /** + * Used by ActionNameAt and DoAction to ensure the index for opening the + * longdesc URL is valid. + * It is always assumed that the highest possible index opens the longdesc. + * This doesn't check that there is actually a longdesc, just that the index + * would be correct if there was one. + * + * @param aIndex The 0-based index to be tested. + * + * @returns true if index is valid for longdesc action. + */ + inline bool IsLongDescIndex(uint8_t aIndex) const; + + uint32_t mImageRequestStatus; +}; + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible downcasting method + +inline ImageAccessible* LocalAccessible::AsImage() { + return IsImage() ? static_cast<ImageAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/LocalAccessible-inl.h b/accessible/generic/LocalAccessible-inl.h new file mode 100644 index 0000000000..4692b85186 --- /dev/null +++ b/accessible/generic/LocalAccessible-inl.h @@ -0,0 +1,114 @@ +/* -*- 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_Accessible_inl_h_ +#define mozilla_a11y_Accessible_inl_h_ + +#include "DocAccessible.h" +#include "ARIAMap.h" +#include "nsCoreUtils.h" +#include "mozilla/dom/Element.h" +#include "mozilla/PresShell.h" + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +namespace mozilla { +namespace a11y { + +inline mozilla::a11y::role LocalAccessible::Role() const { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole) { + return ARIATransformRole(NativeRole()); + } + + return ARIATransformRole(roleMapEntry->role); +} + +inline mozilla::a11y::role LocalAccessible::ARIARole() { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole) { + return mozilla::a11y::roles::NOTHING; + } + + return ARIATransformRole(roleMapEntry->role); +} + +inline void LocalAccessible::SetRoleMapEntry( + const nsRoleMapEntry* aRoleMapEntry) { + mRoleMapEntryIndex = aria::GetIndexFromRoleMap(aRoleMapEntry); +} + +inline bool LocalAccessible::IsSearchbox() const { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return (roleMapEntry && roleMapEntry->Is(nsGkAtoms::searchbox)) || + (mContent->IsHTMLElement(nsGkAtoms::input) && + mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::search, eCaseMatters)); +} + +inline bool LocalAccessible::NativeHasNumericValue() const { + return mGenericTypes & eNumericValue; +} + +inline bool LocalAccessible::ARIAHasNumericValue() const { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) return false; + + if (roleMapEntry->valueRule == eHasValueMinMaxIfFocusable) { + return InteractiveState() & states::FOCUSABLE; + } + + return true; +} + +inline bool LocalAccessible::HasNumericValue() const { + return NativeHasNumericValue() || ARIAHasNumericValue(); +} + +inline bool LocalAccessible::IsDefunct() const { + MOZ_ASSERT(mStateFlags & eIsDefunct || IsApplication() || IsDoc() || + mStateFlags & eSharedNode || mContent, + "No content"); + return mStateFlags & eIsDefunct; +} + +inline void LocalAccessible::ScrollTo(uint32_t aHow) const { + if (mContent) { + RefPtr<PresShell> presShell = mDoc->PresShellPtr(); + nsCOMPtr<nsIContent> content = mContent; + nsCoreUtils::ScrollTo(presShell, content, aHow); + } +} + +inline bool LocalAccessible::InsertAfter(LocalAccessible* aNewChild, + LocalAccessible* aRefChild) { + MOZ_ASSERT(aNewChild, "No new child to insert"); + + if (aRefChild && aRefChild->LocalParent() != this) { +#ifdef A11Y_LOG + logging::TreeInfo("broken accessible tree", 0, "parent", this, + "prev sibling parent", aRefChild->LocalParent(), "child", + aNewChild, nullptr); + if (logging::IsEnabled(logging::eVerbose)) { + logging::Tree("TREE", "Document tree", mDoc); + logging::DOMTree("TREE", "DOM document tree", mDoc); + } +#endif + MOZ_ASSERT_UNREACHABLE("Broken accessible tree"); + mDoc->UnbindFromDocument(aNewChild); + return false; + } + + return InsertChildAt(aRefChild ? aRefChild->IndexInParent() + 1 : 0, + aNewChild); +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp new file mode 100644 index 0000000000..674de4d1ee --- /dev/null +++ b/accessible/generic/LocalAccessible.cpp @@ -0,0 +1,3977 @@ +/* -*- 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 "AccEvent.h" +#include "LocalAccessible-inl.h" + +#include "EmbeddedObjCollector.h" +#include "AccAttributes.h" +#include "AccGroupInfo.h" +#include "AccIterator.h" +#include "CacheConstants.h" +#include "CachedTableAccessible.h" +#include "DocAccessible-inl.h" +#include "mozilla/a11y/AccAttributes.h" +#include "nsAccUtils.h" +#include "nsAccessibilityService.h" +#include "ApplicationAccessible.h" +#include "nsAccessiblePivot.h" +#include "nsGenericHTMLElement.h" +#include "NotificationController.h" +#include "nsEventShell.h" +#include "nsTextEquivUtils.h" +#include "DocAccessibleChild.h" +#include "EventTree.h" +#include "OuterDocAccessible.h" +#include "Pivot.h" +#include "Relation.h" +#include "Role.h" +#include "RootAccessible.h" +#include "States.h" +#include "StyleInfo.h" +#include "TextLeafRange.h" +#include "TextRange.h" +#include "TableAccessible.h" +#include "TableCellAccessible.h" +#include "TreeWalker.h" +#include "HTMLElementAccessibles.h" +#include "HTMLSelectAccessible.h" +#include "ImageAccessible.h" + +#include "nsIDOMXULButtonElement.h" +#include "nsIDOMXULSelectCntrlEl.h" +#include "nsIDOMXULSelectCntrlItemEl.h" +#include "nsINodeList.h" +#include "nsPIDOMWindow.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "mozilla/dom/HTMLAnchorElement.h" +#include "mozilla/gfx/Matrix.h" +#include "nsIContent.h" +#include "nsIFormControl.h" + +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsIFrame.h" +#include "nsTextFrame.h" +#include "nsView.h" +#include "nsIDocShellTreeItem.h" +#include "nsIScrollableFrame.h" +#include "nsStyleStructInlines.h" +#include "nsFocusManager.h" + +#include "nsString.h" +#include "nsUnicharUtils.h" +#include "nsReadableUtils.h" +#include "prdtoa.h" +#include "nsAtom.h" +#include "nsIURI.h" +#include "nsArrayUtils.h" +#include "nsWhitespaceTokenizer.h" +#include "nsAttrName.h" +#include "nsContainerFrame.h" + +#include "mozilla/Assertions.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Components.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/Unused.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/dom/CanvasRenderingContext2D.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "mozilla/dom/HTMLLabelElement.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "mozilla/dom/TreeWalker.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/MutationEventBinding.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible: nsISupports and cycle collection + +NS_IMPL_CYCLE_COLLECTION_CLASS(LocalAccessible) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(LocalAccessible) + tmp->Shutdown(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(LocalAccessible) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent, mDoc) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LocalAccessible) + NS_INTERFACE_MAP_ENTRY_CONCRETE(LocalAccessible) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, LocalAccessible) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(LocalAccessible) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_DESTROY(LocalAccessible, LastRelease()) + +LocalAccessible::LocalAccessible(nsIContent* aContent, DocAccessible* aDoc) + : Accessible(), + mContent(aContent), + mDoc(aDoc), + mParent(nullptr), + mIndexInParent(-1), + mBounds(), + mFirstLineStart(-1), + mStateFlags(0), + mContextFlags(0), + mReorderEventTarget(false), + mShowEventTarget(false), + mHideEventTarget(false), + mIndexOfEmbeddedChild(-1), + mGroupInfo(nullptr) {} + +LocalAccessible::~LocalAccessible() { + NS_ASSERTION(!mDoc, "LastRelease was never called!?!"); +} + +ENameValueFlag LocalAccessible::Name(nsString& aName) const { + aName.Truncate(); + + if (!HasOwnContent()) return eNameOK; + + ARIAName(aName); + if (!aName.IsEmpty()) return eNameOK; + + ENameValueFlag nameFlag = NativeName(aName); + if (!aName.IsEmpty()) return nameFlag; + + // In the end get the name from tooltip. + if (mContent->IsHTMLElement()) { + if (mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::title, + aName)) { + aName.CompressWhitespace(); + return eNameFromTooltip; + } + } else if (mContent->IsXULElement()) { + if (mContent->AsElement()->GetAttr(kNameSpaceID_None, + nsGkAtoms::tooltiptext, aName)) { + aName.CompressWhitespace(); + return eNameFromTooltip; + } + } else if (mContent->IsSVGElement()) { + // If user agents need to choose among multiple 'desc' or 'title' + // elements for processing, the user agent shall choose the first one. + for (nsIContent* childElm = mContent->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (childElm->IsSVGElement(nsGkAtoms::desc)) { + nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName); + return eNameFromTooltip; + } + } + } + + aName.SetIsVoid(true); + + return nameFlag; +} + +void LocalAccessible::Description(nsString& aDescription) const { + // There are 4 conditions that make an accessible have no accDescription: + // 1. it's a text node; or + // 2. It has no ARIA describedby or description property + // 3. it doesn't have an accName; or + // 4. its title attribute already equals to its accName nsAutoString name; + + if (!HasOwnContent() || mContent->IsText()) return; + + ARIADescription(aDescription); + + if (aDescription.IsEmpty()) { + NativeDescription(aDescription); + + if (aDescription.IsEmpty()) { + // Keep the Name() method logic. + if (mContent->IsHTMLElement()) { + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::title, + aDescription); + } else if (mContent->IsXULElement()) { + mContent->AsElement()->GetAttr(kNameSpaceID_None, + nsGkAtoms::tooltiptext, aDescription); + } else if (mContent->IsSVGElement()) { + for (nsIContent* childElm = mContent->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (childElm->IsSVGElement(nsGkAtoms::desc)) { + nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, + &aDescription); + break; + } + } + } + } + } + + if (!aDescription.IsEmpty()) { + aDescription.CompressWhitespace(); + nsAutoString name; + Name(name); + // Don't expose a description if it is the same as the name. + if (aDescription.Equals(name)) aDescription.Truncate(); + } +} + +KeyBinding LocalAccessible::AccessKey() const { + if (!HasOwnContent()) return KeyBinding(); + + uint32_t key = nsCoreUtils::GetAccessKeyFor(mContent); + if (!key && mContent->IsElement()) { + LocalAccessible* label = nullptr; + + // Copy access key from label node. + if (mContent->IsHTMLElement()) { + // Unless it is labeled via an ancestor <label>, in which case that would + // be redundant. + HTMLLabelIterator iter(Document(), this, + HTMLLabelIterator::eSkipAncestorLabel); + label = iter.Next(); + } + if (!label) { + XULLabelIterator iter(Document(), mContent); + label = iter.Next(); + } + + if (label) key = nsCoreUtils::GetAccessKeyFor(label->GetContent()); + } + + if (!key) return KeyBinding(); + + // Get modifier mask. Use ui.key.generalAccessKey (unless it is -1). + switch (StaticPrefs::ui_key_generalAccessKey()) { + case -1: + break; + case dom::KeyboardEvent_Binding::DOM_VK_SHIFT: + return KeyBinding(key, KeyBinding::kShift); + case dom::KeyboardEvent_Binding::DOM_VK_CONTROL: + return KeyBinding(key, KeyBinding::kControl); + case dom::KeyboardEvent_Binding::DOM_VK_ALT: + return KeyBinding(key, KeyBinding::kAlt); + case dom::KeyboardEvent_Binding::DOM_VK_META: + return KeyBinding(key, KeyBinding::kMeta); + default: + return KeyBinding(); + } + + // Determine the access modifier used in this context. + dom::Document* document = mContent->GetComposedDoc(); + if (!document) return KeyBinding(); + + nsCOMPtr<nsIDocShellTreeItem> treeItem(document->GetDocShell()); + if (!treeItem) return KeyBinding(); + + nsresult rv = NS_ERROR_FAILURE; + int32_t modifierMask = 0; + switch (treeItem->ItemType()) { + case nsIDocShellTreeItem::typeChrome: + modifierMask = StaticPrefs::ui_key_chromeAccess(); + rv = NS_OK; + break; + case nsIDocShellTreeItem::typeContent: + modifierMask = StaticPrefs::ui_key_contentAccess(); + rv = NS_OK; + break; + } + + return NS_SUCCEEDED(rv) ? KeyBinding(key, modifierMask) : KeyBinding(); +} + +KeyBinding LocalAccessible::KeyboardShortcut() const { return KeyBinding(); } + +uint64_t LocalAccessible::VisibilityState() const { + if (IPCAccessibilityActive() && + StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // Visibility states must be calculated by RemoteAccessible, so there's no + // point calculating them here. + return 0; + } + nsIFrame* frame = GetFrame(); + if (!frame) { + // Element having display:contents is considered visible semantically, + // despite it doesn't have a visually visible box. + if (nsCoreUtils::IsDisplayContents(mContent)) { + return states::OFFSCREEN; + } + return states::INVISIBLE; + } + + if (!frame->StyleVisibility()->IsVisible()) return states::INVISIBLE; + + // It's invisible if the presshell is hidden by a visibility:hidden element in + // an ancestor document. + if (frame->PresShell()->IsUnderHiddenEmbedderElement()) { + return states::INVISIBLE; + } + + // Offscreen state if the document's visibility state is not visible. + if (Document()->IsHidden()) return states::OFFSCREEN; + + // Walk the parent frame chain to see if the frame is in background tab or + // scrolled out. + nsIFrame* curFrame = frame; + do { + nsView* view = curFrame->GetView(); + if (view && view->GetVisibility() == nsViewVisibility_kHide) { + return states::INVISIBLE; + } + + if (nsLayoutUtils::IsPopup(curFrame)) { + return 0; + } + + if (curFrame->StyleUIReset()->mMozSubtreeHiddenOnlyVisually) { + // Offscreen state for background tab content. + return states::OFFSCREEN; + } + + nsIFrame* parentFrame = curFrame->GetParent(); + // If contained by scrollable frame then check that at least 12 pixels + // around the object is visible, otherwise the object is offscreen. + nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame); + const nscoord kMinPixels = nsPresContext::CSSPixelsToAppUnits(12); + if (scrollableFrame) { + nsRect scrollPortRect = scrollableFrame->GetScrollPortRect(); + nsRect frameRect = nsLayoutUtils::TransformFrameRectToAncestor( + frame, frame->GetRectRelativeToSelf(), parentFrame); + if (!scrollPortRect.Contains(frameRect)) { + scrollPortRect.Deflate(kMinPixels, kMinPixels); + if (!scrollPortRect.Intersects(frameRect)) return states::OFFSCREEN; + } + } + + if (!parentFrame) { + parentFrame = nsLayoutUtils::GetCrossDocParentFrameInProcess(curFrame); + // Even if we couldn't find the parent frame, it might mean we are in an + // out-of-process iframe, try to see if |frame| is scrolled out in an + // scrollable frame in a cross-process ancestor document. + if (!parentFrame && + nsLayoutUtils::FrameIsMostlyScrolledOutOfViewInCrossProcess( + frame, kMinPixels)) { + return states::OFFSCREEN; + } + } + + curFrame = parentFrame; + } while (curFrame); + + // Zero area rects can occur in the first frame of a multi-frame text flow, + // in which case the rendered text is not empty and the frame should not be + // marked invisible. + // XXX Can we just remove this check? Why do we need to mark empty + // text invisible? + if (frame->IsTextFrame() && !(frame->GetStateBits() & NS_FRAME_OUT_OF_FLOW) && + frame->GetRect().IsEmpty()) { + nsIFrame::RenderedText text = frame->GetRenderedText( + 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, + nsIFrame::TrailingWhitespace::DontTrim); + if (text.mString.IsEmpty()) { + return states::INVISIBLE; + } + } + + return 0; +} + +uint64_t LocalAccessible::NativeState() const { + uint64_t state = 0; + + if (!IsInDocument()) state |= states::STALE; + + if (HasOwnContent() && mContent->IsElement()) { + dom::ElementState elementState = mContent->AsElement()->State(); + + if (elementState.HasState(dom::ElementState::INVALID)) { + state |= states::INVALID; + } + + if (elementState.HasState(dom::ElementState::REQUIRED)) { + state |= states::REQUIRED; + } + + state |= NativeInteractiveState(); + } + + // Gather states::INVISIBLE and states::OFFSCREEN flags for this object. + state |= VisibilityState(); + + nsIFrame* frame = GetFrame(); + if (frame) { + if (frame->GetStateBits() & NS_FRAME_OUT_OF_FLOW) state |= states::FLOATING; + + // XXX we should look at layout for non XUL box frames, but need to decide + // how that interacts with ARIA. + if (HasOwnContent() && mContent->IsXULElement() && frame->IsXULBoxFrame()) { + const nsStyleXUL* xulStyle = frame->StyleXUL(); + if (xulStyle && frame->IsXULBoxFrame()) { + // In XUL all boxes are either vertical or horizontal + if (xulStyle->mBoxOrient == StyleBoxOrient::Vertical) { + state |= states::VERTICAL; + } else { + state |= states::HORIZONTAL; + } + } + } + } + + // Check if a XUL element has the popup attribute (an attached popup menu). + if (HasOwnContent() && mContent->IsXULElement() && + mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::popup)) { + state |= states::HASPOPUP; + } + + // Bypass the link states specialization for non links. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->roleRule == kUseNativeRole || + roleMapEntry->role == roles::LINK) { + state |= NativeLinkState(); + } + + return state; +} + +uint64_t LocalAccessible::NativeInteractiveState() const { + if (!mContent->IsElement()) return 0; + + if (NativelyUnavailable()) return states::UNAVAILABLE; + + nsIFrame* frame = GetFrame(); + if (frame && frame->IsFocusable()) return states::FOCUSABLE; + + return 0; +} + +uint64_t LocalAccessible::NativeLinkState() const { return 0; } + +bool LocalAccessible::NativelyUnavailable() const { + if (mContent->IsHTMLElement()) return mContent->AsElement()->IsDisabled(); + + return mContent->IsElement() && mContent->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters); +} + +Accessible* LocalAccessible::ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) { + Accessible* child = LocalChildAtPoint(aX, aY, aWhichChild); + if (aWhichChild != EWhichChildAtPoint::DirectChild && child && + child->IsOuterDoc()) { + child = child->ChildAtPoint(aX, aY, aWhichChild); + } + + return child; +} + +LocalAccessible* LocalAccessible::LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) { + // If we can't find the point in a child, we will return the fallback answer: + // we return |this| if the point is within it, otherwise nullptr. + LocalAccessible* fallbackAnswer = nullptr; + LayoutDeviceIntRect rect = Bounds(); + if (rect.Contains(aX, aY)) fallbackAnswer = this; + + if (nsAccUtils::MustPrune(this)) { // Do not dig any further + return fallbackAnswer; + } + + // Search an accessible at the given point starting from accessible document + // because containing block (see CSS2) for out of flow element (for example, + // absolutely positioned element) may be different from its DOM parent and + // therefore accessible for containing block may be different from accessible + // for DOM parent but GetFrameForPoint() should be called for containing block + // to get an out of flow element. + DocAccessible* accDocument = Document(); + NS_ENSURE_TRUE(accDocument, nullptr); + + nsIFrame* rootFrame = accDocument->GetFrame(); + NS_ENSURE_TRUE(rootFrame, nullptr); + + nsIFrame* startFrame = rootFrame; + + // Check whether the point is at popup content. + nsIWidget* rootWidget = rootFrame->GetView()->GetNearestWidget(nullptr); + NS_ENSURE_TRUE(rootWidget, nullptr); + + LayoutDeviceIntRect rootRect = rootWidget->GetScreenBounds(); + + auto point = LayoutDeviceIntPoint(aX - rootRect.X(), aY - rootRect.Y()); + + nsIFrame* popupFrame = nsLayoutUtils::GetPopupFrameForPoint( + accDocument->PresContext()->GetRootPresContext(), rootWidget, point); + if (popupFrame) { + // If 'this' accessible is not inside the popup then ignore the popup when + // searching an accessible at point. + DocAccessible* popupDoc = + GetAccService()->GetDocAccessible(popupFrame->GetContent()->OwnerDoc()); + LocalAccessible* popupAcc = + popupDoc->GetAccessibleOrContainer(popupFrame->GetContent()); + LocalAccessible* popupChild = this; + while (popupChild && !popupChild->IsDoc() && popupChild != popupAcc) { + popupChild = popupChild->LocalParent(); + } + + if (popupChild == popupAcc) startFrame = popupFrame; + } + + nsPresContext* presContext = startFrame->PresContext(); + nsRect screenRect = startFrame->GetScreenRectInAppUnits(); + nsPoint offset(presContext->DevPixelsToAppUnits(aX) - screenRect.X(), + presContext->DevPixelsToAppUnits(aY) - screenRect.Y()); + + nsIFrame* foundFrame = nsLayoutUtils::GetFrameForPoint( + RelativeTo{startFrame, ViewportType::Visual}, offset); + + nsIContent* content = nullptr; + if (!foundFrame || !(content = foundFrame->GetContent())) { + return fallbackAnswer; + } + + // Get accessible for the node with the point or the first accessible in + // the DOM parent chain. + DocAccessible* contentDocAcc = + GetAccService()->GetDocAccessible(content->OwnerDoc()); + + // contentDocAcc in some circumstances can be nullptr. See bug 729861 + NS_ASSERTION(contentDocAcc, "could not get the document accessible"); + if (!contentDocAcc) return fallbackAnswer; + + LocalAccessible* accessible = + contentDocAcc->GetAccessibleOrContainer(content); + if (!accessible) return fallbackAnswer; + + // Hurray! We have an accessible for the frame that layout gave us. + // Since DOM node of obtained accessible may be out of flow then we should + // ensure obtained accessible is a child of this accessible. + LocalAccessible* child = accessible; + while (child != this) { + LocalAccessible* parent = child->LocalParent(); + if (!parent) { + // Reached the top of the hierarchy. These bounds were inside an + // accessible that is not a descendant of this one. + return fallbackAnswer; + } + + // If we landed on a legitimate child of |this|, and we want the direct + // child, return it here. + if (parent == this && aWhichChild == EWhichChildAtPoint::DirectChild) { + return child; + } + + child = parent; + } + + // Manually walk through accessible children and see if the are within this + // point. Skip offscreen or invisible accessibles. This takes care of cases + // where layout won't walk into things for us, such as image map areas and + // sub documents (XXX: subdocuments should be handled by methods of + // OuterDocAccessibles). + uint32_t childCount = accessible->ChildCount(); + if (childCount == 1 && accessible->IsOuterDoc() && + accessible->FirstChild()->IsRemote()) { + // No local children. + return accessible; + } + for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { + LocalAccessible* child = accessible->LocalChildAt(childIdx); + + LayoutDeviceIntRect childRect = child->Bounds(); + if (childRect.Contains(aX, aY) && + (child->State() & states::INVISIBLE) == 0) { + if (aWhichChild == EWhichChildAtPoint::DeepestChild) { + return child->LocalChildAtPoint(aX, aY, + EWhichChildAtPoint::DeepestChild); + } + + return child; + } + } + + return accessible; +} + +nsIFrame* LocalAccessible::FindNearestAccessibleAncestorFrame() { + nsIFrame* frame = GetFrame(); + if (IsDoc()) { + // We bound documents by their own frame, which is their PresShell's root + // frame. We cache the document offset elsewhere in BundleFieldsForCache + // using the nsGkAtoms::crossorigin attribute. + MOZ_ASSERT(frame, "DocAccessibles should always have a frame"); + return frame; + } + + // Iterate through accessible's ancestors to find one with a frame. + LocalAccessible* ancestor = mParent; + while (ancestor) { + if (nsIFrame* boundingFrame = ancestor->GetFrame()) { + return boundingFrame; + } + ancestor = ancestor->LocalParent(); + } + + MOZ_ASSERT_UNREACHABLE("No ancestor with frame?"); + return nsLayoutUtils::GetContainingBlockForClientRect(frame); +} + +nsRect LocalAccessible::ParentRelativeBounds() { + nsIFrame* frame = GetFrame(); + if (frame && mContent) { + nsIFrame* boundingFrame = FindNearestAccessibleAncestorFrame(); + nsRect result = nsLayoutUtils::GetAllInFlowRectsUnion(frame, boundingFrame); + + if (result.IsEmpty()) { + // If we end up with a 0x0 rect from above (or one with negative + // height/width) we should try using the ink overflow rect instead. If we + // use this rect, our relative bounds will match the bounds of what + // appears visually. We do this because some web authors (icloud.com for + // example) employ things like 0x0 buttons with visual overflow. Without + // this, such frames aren't navigable by screen readers. + result = frame->InkOverflowRectRelativeToSelf(); + nsLayoutUtils::TransformRect(frame, boundingFrame, result); + } + + if (nsIScrollableFrame* sf = + mParent == mDoc + ? mDoc->PresShellPtr()->GetRootScrollFrameAsScrollable() + : boundingFrame->GetScrollTargetFrame()) { + // If boundingFrame has a scroll position, result is currently relative + // to that. Instead, we want result to remain the same regardless of + // scrolling. We then subtract the scroll position later when calculating + // absolute bounds. We do this because we don't want to push cache + // updates for the bounds of all descendants every time we scroll. + nsPoint scrollPos = sf->GetScrollPosition().ApplyResolution( + mDoc->PresShellPtr()->GetResolution()); + result.MoveBy(scrollPos.x, scrollPos.y); + } + + return result; + } + + return nsRect(); +} + +nsRect LocalAccessible::RelativeBounds(nsIFrame** aBoundingFrame) const { + nsIFrame* frame = GetFrame(); + if (frame && mContent) { + *aBoundingFrame = nsLayoutUtils::GetContainingBlockForClientRect(frame); + nsRect unionRect = nsLayoutUtils::GetAllInFlowRectsUnion( + frame, *aBoundingFrame, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS); + + if (unionRect.IsEmpty()) { + // If we end up with a 0x0 rect from above (or one with negative + // height/width) we should try using the ink overflow rect instead. If we + // use this rect, our relative bounds will match the bounds of what + // appears visually. We do this because some web authors (icloud.com for + // example) employ things like 0x0 buttons with visual overflow. Without + // this, such frames aren't navigable by screen readers. + nsRect overflow = frame->InkOverflowRectRelativeToSelf(); + nsLayoutUtils::TransformRect(frame, *aBoundingFrame, overflow); + return overflow; + } + + return unionRect; + } + + return nsRect(); +} + +nsRect LocalAccessible::BoundsInAppUnits() const { + nsIFrame* boundingFrame = nullptr; + nsRect unionRectTwips = RelativeBounds(&boundingFrame); + if (!boundingFrame) { + return nsRect(); + } + + PresShell* presShell = mDoc->PresContext()->PresShell(); + + // We need to inverse translate with the offset of the edge of the visual + // viewport from top edge of the layout viewport. + nsPoint viewportOffset = presShell->GetVisualViewportOffset() - + presShell->GetLayoutViewportOffset(); + unionRectTwips.MoveBy(-viewportOffset); + + // We need to take into account a non-1 resolution set on the presshell. + // This happens with async pinch zooming. Here we scale the bounds before + // adding the screen-relative offset. + unionRectTwips.ScaleRoundOut(presShell->GetResolution()); + // We have the union of the rectangle, now we need to put it in absolute + // screen coords. + nsRect orgRectPixels = boundingFrame->GetScreenRectInAppUnits(); + unionRectTwips.MoveBy(orgRectPixels.X(), orgRectPixels.Y()); + + return unionRectTwips; +} + +LayoutDeviceIntRect LocalAccessible::Bounds() const { + return LayoutDeviceIntRect::FromAppUnitsToNearest( + BoundsInAppUnits(), mDoc->PresContext()->AppUnitsPerDevPixel()); +} + +void LocalAccessible::SetSelected(bool aSelect) { + if (!HasOwnContent()) return; + + LocalAccessible* select = nsAccUtils::GetSelectableContainer(this, State()); + if (select) { + if (select->State() & states::MULTISELECTABLE) { + if (mContent->IsElement() && ARIARoleMap()) { + if (aSelect) { + mContent->AsElement()->SetAttr( + kNameSpaceID_None, nsGkAtoms::aria_selected, u"true"_ns, true); + } else { + mContent->AsElement()->UnsetAttr(kNameSpaceID_None, + nsGkAtoms::aria_selected, true); + } + } + return; + } + + if (aSelect) TakeFocus(); + } +} + +void LocalAccessible::TakeSelection() { + LocalAccessible* select = nsAccUtils::GetSelectableContainer(this, State()); + if (select) { + if (select->State() & states::MULTISELECTABLE) select->UnselectAll(); + SetSelected(true); + } +} + +void LocalAccessible::TakeFocus() const { + nsIFrame* frame = GetFrame(); + if (!frame) return; + + nsIContent* focusContent = mContent; + + // If the accessible focus is managed by container widget then focus the + // widget and set the accessible as its current item. + if (!frame->IsFocusable()) { + LocalAccessible* widget = ContainerWidget(); + if (widget && widget->AreItemsOperable()) { + nsIContent* widgetElm = widget->GetContent(); + nsIFrame* widgetFrame = widgetElm->GetPrimaryFrame(); + if (widgetFrame && widgetFrame->IsFocusable()) { + focusContent = widgetElm; + widget->SetCurrentItem(this); + } + } + } + + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + dom::AutoHandlingUserInputStatePusher inputStatePusher(true); + // XXXbz: Can we actually have a non-element content here? + RefPtr<dom::Element> element = dom::Element::FromNodeOrNull(focusContent); + fm->SetFocus(element, 0); + } +} + +void LocalAccessible::NameFromAssociatedXULLabel(DocAccessible* aDocument, + nsIContent* aElm, + nsString& aName) { + LocalAccessible* label = nullptr; + XULLabelIterator iter(aDocument, aElm); + while ((label = iter.Next())) { + // Check if label's value attribute is used + label->Elm()->GetAttr(kNameSpaceID_None, nsGkAtoms::value, aName); + if (aName.IsEmpty()) { + // If no value attribute, a non-empty label must contain + // children that define its text -- possibly using HTML + nsTextEquivUtils::AppendTextEquivFromContent(label, label->Elm(), &aName); + } + } + aName.CompressWhitespace(); +} + +void LocalAccessible::XULElmName(DocAccessible* aDocument, nsIContent* aElm, + nsString& aName) { + /** + * 3 main cases for XUL Controls to be labeled + * 1 - control contains label="foo" + * 2 - non-child label contains control="controlID" + * - label has either value="foo" or children + * 3 - name from subtree; e.g. a child label element + * Cases 1 and 2 are handled here. + * Case 3 is handled by GetNameFromSubtree called in NativeName. + * Once a label is found, the search is discontinued, so a control + * that has a label attribute as well as having a label external to + * the control that uses the control="controlID" syntax will use + * the label attribute for its Name. + */ + + // CASE #1 (via label attribute) -- great majority of the cases + // Only do this if this is not a select control element, which uses label + // attribute to indicate, which option is selected. + nsCOMPtr<nsIDOMXULSelectControlElement> select = + aElm->AsElement()->AsXULSelectControl(); + if (!select) { + aElm->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName); + } + + // CASE #2 -- label as <label control="id" ... ></label> + if (aName.IsEmpty()) { + NameFromAssociatedXULLabel(aDocument, aElm, aName); + } + + aName.CompressWhitespace(); +} + +nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) { + NS_ENSURE_ARG_POINTER(aEvent); + + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString strEventType; + GetAccService()->GetStringEventType(aEvent->GetEventType(), strEventType); + nsAutoCString strMarker; + strMarker.AppendLiteral("A11y Event - "); + strMarker.Append(strEventType); + PROFILER_MARKER_UNTYPED(strMarker, A11Y); + } + + if (IPCAccessibilityActive() && Document()) { + DocAccessibleChild* ipcDoc = mDoc->IPCDoc(); + // If ipcDoc is null, we can't fire the event to the client. We shouldn't + // have fired the event in the first place, since this makes events + // inconsistent for local and remote documents. To avoid this, don't call + // nsEventShell::FireEvent on a DocAccessible for which + // HasLoadState(eTreeConstructed) is false. + MOZ_ASSERT(ipcDoc); + if (ipcDoc) { + uint64_t id = aEvent->GetAccessible()->ID(); + + switch (aEvent->GetEventType()) { + case nsIAccessibleEvent::EVENT_SHOW: + ipcDoc->ShowEvent(downcast_accEvent(aEvent)); + break; + + case nsIAccessibleEvent::EVENT_HIDE: + ipcDoc->SendHideEvent(id, aEvent->IsFromUserInput()); + break; + + case nsIAccessibleEvent::EVENT_INNER_REORDER: + case nsIAccessibleEvent::EVENT_REORDER: + if (IsTable()) { + SendCache(CacheDomain::Table, CacheUpdateType::Update); + } + +#if defined(XP_WIN) + if (StaticPrefs::accessibility_cache_enabled_AtStartup() && + HasOwnContent() && mContent->IsMathMLElement()) { + // For any change in a MathML subtree, update the innerHTML cache on + // the root math element. + for (LocalAccessible* acc = this; acc; acc = acc->LocalParent()) { + if (acc->HasOwnContent() && + acc->mContent->IsMathMLElement(nsGkAtoms::math)) { + mDoc->QueueCacheUpdate(acc, CacheDomain::InnerHTML); + } + } + } +#endif // defined(XP_WIN) + + // reorder events on the application acc aren't necessary to tell the + // parent about new top level documents. + if (!aEvent->GetAccessible()->IsApplication()) { + ipcDoc->SendEvent(id, aEvent->GetEventType()); + } + break; + case nsIAccessibleEvent::EVENT_STATE_CHANGE: { + AccStateChangeEvent* event = downcast_accEvent(aEvent); + ipcDoc->SendStateChangeEvent(id, event->GetState(), + event->IsStateEnabled()); + break; + } + case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { + AccCaretMoveEvent* event = downcast_accEvent(aEvent); + ipcDoc->SendCaretMoveEvent( + id, event->GetCaretOffset(), event->IsSelectionCollapsed(), + event->IsAtEndOfLine(), event->GetGranularity()); + break; + } + case nsIAccessibleEvent::EVENT_TEXT_INSERTED: + case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { + AccTextChangeEvent* event = downcast_accEvent(aEvent); + const nsString& text = event->ModifiedText(); +#if defined(XP_WIN) + // On Windows with the cache disabled, events for live region updates + // containing embedded objects require us to dispatch synchronous + // events. + bool sync = !StaticPrefs::accessibility_cache_enabled_AtStartup() && + text.Contains(L'\xfffc') && + nsAccUtils::IsARIALive(aEvent->GetAccessible()); +#endif + ipcDoc->SendTextChangeEvent(id, text, event->GetStartOffset(), + event->GetLength(), + event->IsTextInserted(), + event->IsFromUserInput() +#if defined(XP_WIN) + // This parameter only exists on Windows. + , + sync +#endif + ); + break; + } + case nsIAccessibleEvent::EVENT_SELECTION: + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: { + AccSelChangeEvent* selEvent = downcast_accEvent(aEvent); + ipcDoc->SendSelectionEvent(id, selEvent->Widget()->ID(), + aEvent->GetEventType()); + break; + } + case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: { + AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent); + LocalAccessible* position = vcEvent->NewAccessible(); + LocalAccessible* oldPosition = vcEvent->OldAccessible(); + ipcDoc->SendVirtualCursorChangeEvent( + id, oldPosition ? oldPosition->ID() : 0, + vcEvent->OldStartOffset(), vcEvent->OldEndOffset(), + position ? position->ID() : 0, vcEvent->NewStartOffset(), + vcEvent->NewEndOffset(), vcEvent->Reason(), + vcEvent->BoundaryType(), vcEvent->IsFromUserInput()); + break; + } +#if defined(XP_WIN) + case nsIAccessibleEvent::EVENT_FOCUS: { + ipcDoc->SendFocusEvent(id); + break; + } +#endif + case nsIAccessibleEvent::EVENT_SCROLLING_END: + case nsIAccessibleEvent::EVENT_SCROLLING: { + AccScrollingEvent* scrollingEvent = downcast_accEvent(aEvent); + ipcDoc->SendScrollingEvent( + id, aEvent->GetEventType(), scrollingEvent->ScrollX(), + scrollingEvent->ScrollY(), scrollingEvent->MaxScrollX(), + scrollingEvent->MaxScrollY()); + break; + } +#if !defined(XP_WIN) + case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: { + AccAnnouncementEvent* announcementEvent = downcast_accEvent(aEvent); + ipcDoc->SendAnnouncementEvent(id, announcementEvent->Announcement(), + announcementEvent->Priority()); + break; + } +#endif // !defined(XP_WIN) + case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: { +#if defined(XP_WIN) + if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // On Windows, when the cache is disabled, we have to defer events + // until we are notified that the DocAccessibleParent has been + // constructed, which needs specific code for each event payload. + // Since we don't need a special event payload for text selection in + // this case anyway, just send it as a generic event. + ipcDoc->SendEvent(id, aEvent->GetEventType()); + break; + } +#endif // defined(XP_WIN) + AccTextSelChangeEvent* textSelChangeEvent = downcast_accEvent(aEvent); + AutoTArray<TextRange, 1> ranges; + textSelChangeEvent->SelectionRanges(&ranges); + nsTArray<TextRangeData> textRangeData(ranges.Length()); + for (size_t i = 0; i < ranges.Length(); i++) { + const TextRange& range = ranges.ElementAt(i); + LocalAccessible* start = range.StartContainer()->AsLocal(); + LocalAccessible* end = range.EndContainer()->AsLocal(); + textRangeData.AppendElement(TextRangeData(start->ID(), end->ID(), + range.StartOffset(), + range.EndOffset())); + } + ipcDoc->SendTextSelectionChangeEvent(id, textRangeData); + break; + } + case nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE: + case nsIAccessibleEvent::EVENT_NAME_CHANGE: { + SendCache(CacheDomain::NameAndDescription, CacheUpdateType::Update); + ipcDoc->SendEvent(id, aEvent->GetEventType()); + break; + } + case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE: + case nsIAccessibleEvent::EVENT_VALUE_CHANGE: { + SendCache(CacheDomain::Value, CacheUpdateType::Update); + ipcDoc->SendEvent(id, aEvent->GetEventType()); + break; + } + default: + ipcDoc->SendEvent(id, aEvent->GetEventType()); + } + } + } + + if (nsCoreUtils::AccEventObserversExist()) { + nsCoreUtils::DispatchAccEvent(MakeXPCEvent(aEvent)); + } + + return NS_OK; +} + +already_AddRefed<AccAttributes> LocalAccessible::Attributes() { + RefPtr<AccAttributes> attributes = NativeAttributes(); + if (!HasOwnContent() || !mContent->IsElement()) return attributes.forget(); + + // 'xml-roles' attribute coming from ARIA. + nsString xmlRoles; + if (nsAccUtils::GetARIAAttr(mContent->AsElement(), nsGkAtoms::role, + xmlRoles) && + !xmlRoles.IsEmpty()) { + attributes->SetAttribute(nsGkAtoms::xmlroles, std::move(xmlRoles)); + } else if (nsAtom* landmark = LandmarkRole()) { + // 'xml-roles' attribute for landmark. + attributes->SetAttribute(nsGkAtoms::xmlroles, landmark); + } + + // Expose object attributes from ARIA attributes. + aria::AttrIterator attribIter(mContent); + while (attribIter.Next()) { + attribIter.ExposeAttr(attributes); + } + + // If there is no aria-live attribute then expose default value of 'live' + // object attribute used for ARIA role of this accessible. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry) { + if (roleMapEntry->Is(nsGkAtoms::searchbox)) { + attributes->SetAttribute(nsGkAtoms::textInputType, nsGkAtoms::search); + } + + if (!attributes->HasAttribute(nsGkAtoms::aria_live)) { + nsString live; + if (nsAccUtils::GetLiveAttrValue(roleMapEntry->liveAttRule, live)) { + attributes->SetAttribute(nsGkAtoms::aria_live, std::move(live)); + } + } + } + + return attributes.forget(); +} + +already_AddRefed<AccAttributes> LocalAccessible::NativeAttributes() { + RefPtr<AccAttributes> attributes = new AccAttributes(); + + // We support values, so expose the string value as well, via the valuetext + // object attribute. We test for the value interface because we don't want + // to expose traditional Value() information such as URL's on links and + // documents, or text in an input. + if (HasNumericValue()) { + nsString valuetext; + Value(valuetext); + attributes->SetAttribute(nsGkAtoms::aria_valuetext, std::move(valuetext)); + } + + // Expose checkable object attribute if the accessible has checkable state + if (State() & states::CHECKABLE) { + attributes->SetAttribute(nsGkAtoms::checkable, true); + } + + // Expose 'explicit-name' attribute. + nsAutoString name; + if (Name(name) != eNameFromSubtree && !name.IsVoid()) { + attributes->SetAttribute(nsGkAtoms::explicit_name, true); + } + + // Group attributes (level/setsize/posinset) + GroupPos groupPos = GroupPosition(); + nsAccUtils::SetAccGroupAttrs(attributes, groupPos.level, groupPos.setSize, + groupPos.posInSet); + + bool hierarchical = false; + uint32_t itemCount = AccGroupInfo::TotalItemCount(this, &hierarchical); + if (itemCount) { + attributes->SetAttribute(nsGkAtoms::child_item_count, + static_cast<int32_t>(itemCount)); + } + + if (hierarchical) { + attributes->SetAttribute(nsGkAtoms::tree, true); + } + + // If the accessible doesn't have own content (such as list item bullet or + // xul tree item) then don't calculate content based attributes. + if (!HasOwnContent()) return attributes.forget(); + + nsEventShell::GetEventAttributes(GetNode(), attributes); + + // Get container-foo computed live region properties based on the closest + // container with the live region attribute. Inner nodes override outer nodes + // within the same document. The inner nodes can be used to override live + // region behavior on more general outer nodes. + nsAccUtils::SetLiveContainerAttributes(attributes, this); + + if (!mContent->IsElement()) return attributes.forget(); + + nsString id; + if (nsCoreUtils::GetID(mContent, id)) { + attributes->SetAttribute(nsGkAtoms::id, std::move(id)); + } + + // Expose class because it may have useful microformat information. + nsString _class; + if (mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::_class, + _class)) { + attributes->SetAttribute(nsGkAtoms::_class, std::move(_class)); + } + + // Expose tag. + attributes->SetAttribute(nsGkAtoms::tag, mContent->NodeInfo()->NameAtom()); + + // Expose draggable object attribute. + if (auto htmlElement = nsGenericHTMLElement::FromNode(mContent)) { + if (htmlElement->Draggable()) { + attributes->SetAttribute(nsGkAtoms::draggable, true); + } + } + + // Don't calculate CSS-based object attributes when: + // 1. There is no frame (e.g. the accessible is unattached from the tree). + // 2. This is an image map area. CSS is irrelevant here. Furthermore, we won't + // be able to get the computed style if the map is unslotted in a shadow host. + if (!mContent->GetPrimaryFrame() || + mContent->IsHTMLElement(nsGkAtoms::area)) { + return attributes.forget(); + } + + // CSS style based object attributes. + nsAutoString value; + StyleInfo styleInfo(mContent->AsElement()); + + // Expose 'display' attribute. + RefPtr<nsAtom> displayValue = styleInfo.Display(); + attributes->SetAttribute(nsGkAtoms::display, displayValue); + + // Expose 'text-align' attribute. + RefPtr<nsAtom> textAlignValue = styleInfo.TextAlign(); + attributes->SetAttribute(nsGkAtoms::textAlign, textAlignValue); + + // Expose 'text-indent' attribute. + mozilla::LengthPercentage textIndent = styleInfo.TextIndent(); + if (textIndent.ConvertsToLength()) { + attributes->SetAttribute(nsGkAtoms::textIndent, + textIndent.ToLengthInCSSPixels()); + } else if (textIndent.ConvertsToPercentage()) { + attributes->SetAttribute(nsGkAtoms::textIndent, textIndent.ToPercentage()); + } + + // Expose 'margin-left' attribute. + attributes->SetAttribute(nsGkAtoms::marginLeft, styleInfo.MarginLeft()); + + // Expose 'margin-right' attribute. + attributes->SetAttribute(nsGkAtoms::marginRight, styleInfo.MarginRight()); + + // Expose 'margin-top' attribute. + attributes->SetAttribute(nsGkAtoms::marginTop, styleInfo.MarginTop()); + + // Expose 'margin-bottom' attribute. + attributes->SetAttribute(nsGkAtoms::marginBottom, styleInfo.MarginBottom()); + + // Expose data-at-shortcutkeys attribute for web applications and virtual + // cursors. Currently mostly used by JAWS. + nsString atShortcutKeys; + if (mContent->AsElement()->GetAttr( + kNameSpaceID_None, nsGkAtoms::dataAtShortcutkeys, atShortcutKeys)) { + attributes->SetAttribute(nsGkAtoms::dataAtShortcutkeys, + std::move(atShortcutKeys)); + } + + return attributes.forget(); +} + +bool LocalAccessible::AttributeChangesState(nsAtom* aAttribute) { + return aAttribute == nsGkAtoms::aria_disabled || + aAttribute == nsGkAtoms::disabled || + aAttribute == nsGkAtoms::tabindex || + aAttribute == nsGkAtoms::aria_required || + aAttribute == nsGkAtoms::aria_invalid || + aAttribute == nsGkAtoms::aria_expanded || + aAttribute == nsGkAtoms::aria_checked || + (aAttribute == nsGkAtoms::aria_pressed && IsButton()) || + aAttribute == nsGkAtoms::aria_readonly || + aAttribute == nsGkAtoms::aria_current || + aAttribute == nsGkAtoms::aria_haspopup || + aAttribute == nsGkAtoms::aria_busy || + aAttribute == nsGkAtoms::aria_multiline || + aAttribute == nsGkAtoms::aria_multiselectable || + aAttribute == nsGkAtoms::contenteditable; +} + +void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue, + uint64_t aOldState) { + // Fire accessible event after short timer, because we need to wait for + // DOM attribute & resulting layout to actually change. Otherwise, + // assistive technology will retrieve the wrong state/value/selection info. + + // XXX todo + // We still need to handle special HTML cases here + // For example, if an <img>'s usemap attribute is modified + // Otherwise it may just be a state change, for example an object changing + // its visibility + // + // XXX todo: report aria state changes for "undefined" literal value changes + // filed as bug 472142 + // + // XXX todo: invalidate accessible when aria state changes affect exposed + // role filed as bug 472143 + + if (AttributeChangesState(aAttribute)) { + uint64_t currState = State(); + uint64_t diffState = currState ^ aOldState; + if (diffState) { + for (uint64_t state = 1; state <= states::LAST_ENTRY; state <<= 1) { + if (diffState & state) { + RefPtr<AccEvent> stateChangeEvent = + new AccStateChangeEvent(this, state, (currState & state)); + mDoc->FireDelayedEvent(stateChangeEvent); + } + } + } + } + + // When a details object has its open attribute changed + // we should fire a state-change event on the accessible of + // its main summary + if (aAttribute == nsGkAtoms::open) { + // FromDetails checks if the given accessible belongs to + // a details frame and also locates the accessible of its + // main summary. + if (HTMLSummaryAccessible* summaryAccessible = + HTMLSummaryAccessible::FromDetails(this)) { + RefPtr<AccEvent> expandedChangeEvent = + new AccStateChangeEvent(summaryAccessible, states::EXPANDED); + mDoc->FireDelayedEvent(expandedChangeEvent); + return; + } + } + + // Check for namespaced ARIA attribute + if (aNameSpaceID == kNameSpaceID_None) { + // Check for hyphenated aria-foo property? + if (StringBeginsWith(nsDependentAtomString(aAttribute), u"aria-"_ns)) { + uint8_t attrFlags = aria::AttrCharacteristicsFor(aAttribute); + if (!(attrFlags & ATTR_BYPASSOBJ)) { + mDoc->QueueCacheUpdate(this, CacheDomain::ARIA); + // For aria attributes like drag and drop changes we fire a generic + // attribute change event; at least until native API comes up with a + // more meaningful event. + RefPtr<AccEvent> event = + new AccObjectAttrChangedEvent(this, aAttribute); + mDoc->FireDelayedEvent(event); + } + } + } + + dom::Element* elm = Elm(); + + if (HasNumericValue() && + (aAttribute == nsGkAtoms::aria_valuemax || + aAttribute == nsGkAtoms::aria_valuemin || aAttribute == nsGkAtoms::min || + aAttribute == nsGkAtoms::max || aAttribute == nsGkAtoms::step)) { + SendCache(CacheDomain::Value, CacheUpdateType::Update); + return; + } + + // Fire text value change event whenever aria-valuetext is changed. + if (aAttribute == nsGkAtoms::aria_valuetext) { + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, this); + return; + } + + if (aAttribute == nsGkAtoms::aria_valuenow) { + if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_valuetext) || + nsAccUtils::ARIAAttrValueIs(elm, nsGkAtoms::aria_valuetext, + nsGkAtoms::_empty, eCaseMatters)) { + // Fire numeric value change event when aria-valuenow is changed and + // aria-valuetext is empty + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, this); + } else { + // We need to update the cache here since we won't get an event if + // aria-valuenow is shadowed by aria-valuetext. + SendCache(CacheDomain::Value, CacheUpdateType::Update); + } + return; + } + + if (aAttribute == nsGkAtoms::aria_owns) { + mDoc->Controller()->ScheduleRelocation(this); + } + + // Fire name change and description change events. + if (aAttribute == nsGkAtoms::aria_label) { + // A valid aria-labelledby would take precedence so an aria-label change + // won't change the name. + IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_labelledby); + if (!iter.NextElem()) { + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); + } + return; + } + + if (aAttribute == nsGkAtoms::aria_describedby) { + mDoc->QueueCacheUpdate(this, CacheDomain::Relations); + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, this); + if (aModType == dom::MutationEvent_Binding::MODIFICATION || + aModType == dom::MutationEvent_Binding::ADDITION) { + // The subtrees of the new aria-describedby targets might be used to + // compute the description for this. Therefore, we need to set + // the eHasDescriptionDependent flag on all Accessibles in these subtrees. + IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_describedby); + while (LocalAccessible* target = iter.Next()) { + target->ModifySubtreeContextFlags(eHasDescriptionDependent, true); + } + } + return; + } + + if (aAttribute == nsGkAtoms::aria_labelledby) { + // We only queue cache updates for explicit relations. Implicit, reverse + // relations are handled in ApplyCache and stored in a map on the remote + // document itself. + mDoc->QueueCacheUpdate(this, CacheDomain::Relations); + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); + if (aModType == dom::MutationEvent_Binding::MODIFICATION || + aModType == dom::MutationEvent_Binding::ADDITION) { + // The subtrees of the new aria-labelledby targets might be used to + // compute the name for this. Therefore, we need to set + // the eHasNameDependent flag on all Accessibles in these subtrees. + IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_labelledby); + while (LocalAccessible* target = iter.Next()) { + target->ModifySubtreeContextFlags(eHasNameDependent, true); + } + } + return; + } + + if ((aAttribute == nsGkAtoms::aria_expanded || + aAttribute == nsGkAtoms::href) && + (aModType == dom::MutationEvent_Binding::ADDITION || + aModType == dom::MutationEvent_Binding::REMOVAL)) { + // The presence of aria-expanded adds an expand/collapse action. + SendCache(CacheDomain::Actions, CacheUpdateType::Update); + } + + if (aAttribute == nsGkAtoms::href) { + mDoc->QueueCacheUpdate(this, CacheDomain::Value); + } + + if (aAttribute == nsGkAtoms::aria_controls || + aAttribute == nsGkAtoms::aria_flowto) { + mDoc->QueueCacheUpdate(this, CacheDomain::Relations); + } + + if (aAttribute == nsGkAtoms::alt && + !nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label) && + !elm->HasAttr(nsGkAtoms::aria_labelledby)) { + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); + return; + } + + if (aAttribute == nsGkAtoms::title) { + nsAutoString name; + ARIAName(name); + if (name.IsEmpty()) { + NativeName(name); + if (name.IsEmpty()) { + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); + return; + } + } + + if (!elm->HasAttr(nsGkAtoms::aria_describedby)) { + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, + this); + } + + return; + } + + // ARIA or XUL selection + if ((mContent->IsXULElement() && aAttribute == nsGkAtoms::selected) || + aAttribute == nsGkAtoms::aria_selected) { + LocalAccessible* widget = nsAccUtils::GetSelectableContainer(this, State()); + if (widget) { + AccSelChangeEvent::SelChangeType selChangeType; + if (aNameSpaceID != kNameSpaceID_None) { + selChangeType = elm->AttrValueIs(aNameSpaceID, aAttribute, + nsGkAtoms::_true, eCaseMatters) + ? AccSelChangeEvent::eSelectionAdd + : AccSelChangeEvent::eSelectionRemove; + } else { + selChangeType = nsAccUtils::ARIAAttrValueIs( + elm, aAttribute, nsGkAtoms::_true, eCaseMatters) + ? AccSelChangeEvent::eSelectionAdd + : AccSelChangeEvent::eSelectionRemove; + } + + RefPtr<AccEvent> event = + new AccSelChangeEvent(widget, this, selChangeType); + mDoc->FireDelayedEvent(event); + if (aAttribute == nsGkAtoms::aria_selected) { + mDoc->QueueCacheUpdate(this, CacheDomain::State); + } + } + + return; + } + + if (aAttribute == nsGkAtoms::aria_level || + aAttribute == nsGkAtoms::aria_setsize || + aAttribute == nsGkAtoms::aria_posinset) { + SendCache(CacheDomain::GroupInfo, CacheUpdateType::Update); + return; + } + + if (aAttribute == nsGkAtoms::accesskey) { + mDoc->QueueCacheUpdate(this, CacheDomain::Actions); + } + + if (aAttribute == nsGkAtoms::name && + (mContent && mContent->IsHTMLElement(nsGkAtoms::a))) { + // If an anchor's name changed, it's possible a LINKS_TO relation + // also changed. Push a cache update for Relations. + mDoc->QueueCacheUpdate(this, CacheDomain::Relations); + } + + if (aAttribute == nsGkAtoms::slot && + !mContent->GetFlattenedTreeParentNode() && this != mDoc) { + // This is inside a shadow host but is no longer slotted. + mDoc->ContentRemoved(this); + } +} + +void LocalAccessible::ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize, + int32_t* aPosInSet) const { + if (!mContent) { + return; + } + + if (aLevel) { + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_level, aLevel); + } + if (aSetSize) { + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_setsize, aSetSize); + } + if (aPosInSet) { + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_posinset, aPosInSet); + } +} + +uint64_t LocalAccessible::State() { + if (IsDefunct()) return states::DEFUNCT; + + uint64_t state = NativeState(); + // Apply ARIA states to be sure accessible states will be overridden. + ApplyARIAState(&state); + + const uint32_t kExpandCollapseStates = states::COLLAPSED | states::EXPANDED; + if ((state & kExpandCollapseStates) == kExpandCollapseStates) { + // Cannot be both expanded and collapsed -- this happens in ARIA expanded + // combobox because of limitation of ARIAMap. + // XXX: Perhaps we will be able to make this less hacky if we support + // extended states in ARIAMap, e.g. derive COLLAPSED from + // EXPANDABLE && !EXPANDED. + state &= ~states::COLLAPSED; + } + + if (!(state & states::UNAVAILABLE)) { + state |= states::ENABLED | states::SENSITIVE; + + // If the object is a current item of container widget then mark it as + // ACTIVE. This allows screen reader virtual buffer modes to know which + // descendant is the current one that would get focus if the user navigates + // to the container widget. + LocalAccessible* widget = ContainerWidget(); + if (widget && widget->CurrentItem() == this) state |= states::ACTIVE; + } + + if ((state & states::COLLAPSED) || (state & states::EXPANDED)) { + state |= states::EXPANDABLE; + } + + ApplyImplicitState(state); + return state; +} + +void LocalAccessible::ApplyARIAState(uint64_t* aState) const { + if (!mContent->IsElement()) return; + + dom::Element* element = mContent->AsElement(); + + // Test for universal states first + *aState |= aria::UniversalStatesFor(element); + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry) { + // We only force the readonly bit off if we have a real mapping for the aria + // role. This preserves the ability for screen readers to use readonly + // (primarily on the document) as the hint for creating a virtual buffer. + if (roleMapEntry->role != roles::NOTHING) *aState &= ~states::READONLY; + + if (mContent->HasID()) { + // If has a role & ID and aria-activedescendant on the container, assume + // focusable. + const LocalAccessible* ancestor = this; + while ((ancestor = ancestor->LocalParent()) && !ancestor->IsDoc()) { + dom::Element* el = ancestor->Elm(); + if (el && el->HasAttr(nsGkAtoms::aria_activedescendant)) { + *aState |= states::FOCUSABLE; + break; + } + } + } + } + + if (*aState & states::FOCUSABLE) { + // Propogate aria-disabled from ancestors down to any focusable descendant. + const LocalAccessible* ancestor = this; + while ((ancestor = ancestor->LocalParent()) && !ancestor->IsDoc()) { + dom::Element* el = ancestor->Elm(); + if (el && nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::aria_disabled, + nsGkAtoms::_true, eCaseMatters)) { + *aState |= states::UNAVAILABLE; + break; + } + } + } else { + // Sometimes, we use aria-activedescendant targeting something which isn't + // actually a descendant. This is technically a spec violation, but it's a + // useful hack which makes certain things much easier. For example, we use + // this for "fake focus" for multi select browser tabs and Quantumbar + // autocomplete suggestions. + // In these cases, the aria-activedescendant code above won't make the + // active item focusable. It doesn't make sense for something to have + // focus when it isn't focusable, so fix that here. + if (FocusMgr()->IsActiveItem(this)) { + *aState |= states::FOCUSABLE; + } + } + + // special case: A native button element whose role got transformed by ARIA to + // a toggle button Also applies to togglable button menus, like in the Dev + // Tools Web Console. + if (IsButton() || IsMenuButton()) { + aria::MapToState(aria::eARIAPressed, element, aState); + } + + if (!roleMapEntry) return; + + *aState |= roleMapEntry->state; + + if (aria::MapToState(roleMapEntry->attributeMap1, element, aState) && + aria::MapToState(roleMapEntry->attributeMap2, element, aState) && + aria::MapToState(roleMapEntry->attributeMap3, element, aState)) { + aria::MapToState(roleMapEntry->attributeMap4, element, aState); + } + + // ARIA gridcell inherits readonly state from the grid until it's overridden. + if ((roleMapEntry->Is(nsGkAtoms::gridcell) || + roleMapEntry->Is(nsGkAtoms::columnheader) || + roleMapEntry->Is(nsGkAtoms::rowheader)) && + !nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_readonly)) { + const TableCellAccessible* cell = AsTableCell(); + if (cell) { + TableAccessible* table = cell->Table(); + if (table) { + LocalAccessible* grid = table->AsAccessible(); + uint64_t gridState = 0; + grid->ApplyARIAState(&gridState); + *aState |= gridState & states::READONLY; + } + } + } +} + +void LocalAccessible::Value(nsString& aValue) const { + if (HasNumericValue()) { + // aria-valuenow is a number, and aria-valuetext is the optional text + // equivalent. For the string value, we will try the optional text + // equivalent first. + if (!mContent->IsElement()) { + return; + } + + if (!nsAccUtils::GetARIAAttr(mContent->AsElement(), + nsGkAtoms::aria_valuetext, aValue)) { + if (!NativeHasNumericValue()) { + double checkValue = CurValue(); + if (!IsNaN(checkValue)) { + aValue.AppendFloat(checkValue); + } + } + } + return; + } + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry) { + return; + } + + // Value of textbox is a textified subtree. + if (roleMapEntry->Is(nsGkAtoms::textbox)) { + nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue); + return; + } + + // Value of combobox is a text of current or selected item. + if (roleMapEntry->Is(nsGkAtoms::combobox)) { + LocalAccessible* option = CurrentItem(); + if (!option) { + uint32_t childCount = ChildCount(); + for (uint32_t idx = 0; idx < childCount; idx++) { + LocalAccessible* child = mChildren.ElementAt(idx); + if (child->IsListControl()) { + Accessible* acc = child->GetSelectedItem(0); + option = acc ? acc->AsLocal() : nullptr; + break; + } + } + } + + // If there's a selected item, get the value from it. Otherwise, determine + // the value from descendant elements. + nsTextEquivUtils::GetTextEquivFromSubtree(option ? option : this, aValue); + } +} + +double LocalAccessible::MaxValue() const { + double checkValue = AttrNumericValue(nsGkAtoms::aria_valuemax); + if (IsNaN(checkValue) && !NativeHasNumericValue()) { + // aria-valuemax isn't present and this element doesn't natively provide a + // maximum value. Use the ARIA default. + const nsRoleMapEntry* roleMap = ARIARoleMap(); + if (roleMap && roleMap->role == roles::SPINBUTTON) { + return UnspecifiedNaN<double>(); + } + return 100; + } + return checkValue; +} + +double LocalAccessible::MinValue() const { + double checkValue = AttrNumericValue(nsGkAtoms::aria_valuemin); + if (IsNaN(checkValue) && !NativeHasNumericValue()) { + // aria-valuemin isn't present and this element doesn't natively provide a + // minimum value. Use the ARIA default. + const nsRoleMapEntry* roleMap = ARIARoleMap(); + if (roleMap && roleMap->role == roles::SPINBUTTON) { + return UnspecifiedNaN<double>(); + } + return 0; + } + return checkValue; +} + +double LocalAccessible::Step() const { + return UnspecifiedNaN<double>(); // no mimimum increment (step) in ARIA. +} + +double LocalAccessible::CurValue() const { + double checkValue = AttrNumericValue(nsGkAtoms::aria_valuenow); + if (IsNaN(checkValue) && !NativeHasNumericValue()) { + // aria-valuenow isn't present and this element doesn't natively provide a + // current value. Use the ARIA default. + const nsRoleMapEntry* roleMap = ARIARoleMap(); + if (roleMap && roleMap->role == roles::SPINBUTTON) { + return UnspecifiedNaN<double>(); + } + double minValue = MinValue(); + return minValue + ((MaxValue() - minValue) / 2); + } + + return checkValue; +} + +bool LocalAccessible::SetCurValue(double aValue) { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) return false; + + const uint32_t kValueCannotChange = states::READONLY | states::UNAVAILABLE; + if (State() & kValueCannotChange) return false; + + double checkValue = MinValue(); + if (!IsNaN(checkValue) && aValue < checkValue) return false; + + checkValue = MaxValue(); + if (!IsNaN(checkValue) && aValue > checkValue) return false; + + nsAutoString strValue; + strValue.AppendFloat(aValue); + + if (!mContent->IsElement()) return true; + + return NS_SUCCEEDED(mContent->AsElement()->SetAttr( + kNameSpaceID_None, nsGkAtoms::aria_valuenow, strValue, true)); +} + +role LocalAccessible::ARIATransformRole(role aRole) const { + // Beginning with ARIA 1.1, user agents are expected to use the native host + // language role of the element when the region role is used without a name. + // https://rawgit.com/w3c/aria/master/core-aam/core-aam.html#role-map-region + // + // XXX: While the name computation algorithm can be non-trivial in the general + // case, it should not be especially bad here: If the author hasn't used the + // region role, this calculation won't occur. And the region role's name + // calculation rule excludes name from content. That said, this use case is + // another example of why we should consider caching the accessible name. See: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1378235. + if (aRole == roles::REGION) { + nsAutoString name; + Name(name); + return name.IsEmpty() ? NativeRole() : aRole; + } + + // XXX: these unfortunate exceptions don't fit into the ARIA table. This is + // where the accessible role depends on both the role and ARIA state. + if (aRole == roles::PUSHBUTTON) { + if (nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_pressed)) { + // For simplicity, any existing pressed attribute except "" or "undefined" + // indicates a toggle. + return roles::TOGGLE_BUTTON; + } + + if (mContent->IsElement() && + nsAccUtils::ARIAAttrValueIs(mContent->AsElement(), + nsGkAtoms::aria_haspopup, nsGkAtoms::_true, + eCaseMatters)) { + // For button with aria-haspopup="true". + return roles::BUTTONMENU; + } + + } else if (aRole == roles::LISTBOX) { + // A listbox inside of a combobox needs a special role because of ATK + // mapping to menu. + if (mParent && mParent->IsCombobox()) { + return roles::COMBOBOX_LIST; + } + + } else if (aRole == roles::OPTION) { + if (mParent && mParent->Role() == roles::COMBOBOX_LIST) { + return roles::COMBOBOX_OPTION; + } + + } else if (aRole == roles::MENUITEM) { + // Menuitem has a submenu. + if (mContent->IsElement() && + nsAccUtils::ARIAAttrValueIs(mContent->AsElement(), + nsGkAtoms::aria_haspopup, nsGkAtoms::_true, + eCaseMatters)) { + return roles::PARENT_MENUITEM; + } + + } else if (aRole == roles::CELL) { + // A cell inside an ancestor table element that has a grid role needs a + // gridcell role + // (https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings). + const TableCellAccessible* cell = AsTableCell(); + if (cell) { + TableAccessible* table = cell->Table(); + if (table && table->AsAccessible()->IsARIARole(nsGkAtoms::grid)) { + return roles::GRID_CELL; + } + } + } + + return aRole; +} + +role LocalAccessible::NativeRole() const { return roles::NOTHING; } + +uint8_t LocalAccessible::ActionCount() const { + return HasPrimaryAction() || ActionAncestor() ? 1 : 0; +} + +void LocalAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { + aName.Truncate(); + + if (aIndex != 0) return; + + uint32_t actionRule = GetActionRule(); + + switch (actionRule) { + case eActivateAction: + aName.AssignLiteral("activate"); + return; + + case eClickAction: + aName.AssignLiteral("click"); + return; + + case ePressAction: + aName.AssignLiteral("press"); + return; + + case eCheckUncheckAction: { + uint64_t state = State(); + if (state & states::CHECKED) { + aName.AssignLiteral("uncheck"); + } else if (state & states::MIXED) { + aName.AssignLiteral("cycle"); + } else { + aName.AssignLiteral("check"); + } + return; + } + + case eJumpAction: + aName.AssignLiteral("jump"); + return; + + case eOpenCloseAction: + if (State() & states::COLLAPSED) { + aName.AssignLiteral("open"); + } else { + aName.AssignLiteral("close"); + } + return; + + case eSelectAction: + aName.AssignLiteral("select"); + return; + + case eSwitchAction: + aName.AssignLiteral("switch"); + return; + + case eSortAction: + aName.AssignLiteral("sort"); + return; + + case eExpandAction: + if (State() & states::COLLAPSED) { + aName.AssignLiteral("expand"); + } else { + aName.AssignLiteral("collapse"); + } + return; + } + + if (ActionAncestor()) { + aName.AssignLiteral("click ancestor"); + return; + } +} + +bool LocalAccessible::DoAction(uint8_t aIndex) const { + if (aIndex != 0) return false; + + if (HasPrimaryAction() || ActionAncestor()) { + DoCommand(); + return true; + } + + return false; +} + +bool LocalAccessible::HasPrimaryAction() const { + return GetActionRule() != eNoAction; +} + +nsIContent* LocalAccessible::GetAtomicRegion() const { + nsIContent* loopContent = mContent; + nsAutoString atomic; + while (loopContent && + (!loopContent->IsElement() || + !nsAccUtils::GetARIAAttr(loopContent->AsElement(), + nsGkAtoms::aria_atomic, atomic))) { + loopContent = loopContent->GetParent(); + } + + return atomic.EqualsLiteral("true") ? loopContent : nullptr; +} + +Relation LocalAccessible::RelationByType(RelationType aType) const { + if (!HasOwnContent()) return Relation(); + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + + // Relationships are defined on the same content node that the role would be + // defined on. + switch (aType) { + case RelationType::LABELLED_BY: { + Relation rel( + new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_labelledby)); + if (mContent->IsHTMLElement()) { + rel.AppendIter(new HTMLLabelIterator(Document(), this)); + } + rel.AppendIter(new XULLabelIterator(Document(), mContent)); + + return rel; + } + + case RelationType::LABEL_FOR: { + Relation rel(new RelatedAccIterator(Document(), mContent, + nsGkAtoms::aria_labelledby)); + if (mContent->IsXULElement(nsGkAtoms::label)) { + rel.AppendIter(new IDRefsIterator(mDoc, mContent, nsGkAtoms::control)); + } + + return rel; + } + + case RelationType::DESCRIBED_BY: { + Relation rel( + new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_describedby)); + if (mContent->IsXULElement()) { + rel.AppendIter(new XULDescriptionIterator(Document(), mContent)); + } + + return rel; + } + + case RelationType::DESCRIPTION_FOR: { + Relation rel(new RelatedAccIterator(Document(), mContent, + nsGkAtoms::aria_describedby)); + + // This affectively adds an optional control attribute to xul:description, + // which only affects accessibility, by allowing the description to be + // tied to a control. + if (mContent->IsXULElement(nsGkAtoms::description)) { + rel.AppendIter(new IDRefsIterator(mDoc, mContent, nsGkAtoms::control)); + } + + return rel; + } + + case RelationType::NODE_CHILD_OF: { + Relation rel; + // This is an ARIA tree or treegrid that doesn't use owns, so we need to + // get the parent the hard way. + if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM || + roleMapEntry->role == roles::LISTITEM || + roleMapEntry->role == roles::ROW)) { + Accessible* parent = const_cast<LocalAccessible*>(this) + ->GetOrCreateGroupInfo() + ->ConceptualParent(); + if (parent) { + MOZ_ASSERT(parent->IsLocal()); + rel.AppendTarget(parent->AsLocal()); + } + } + + // If this is an OOP iframe document, we can't support NODE_CHILD_OF + // here, since the iframe resides in a different process. This is fine + // because the client will then request the parent instead, which will be + // correctly handled by platform code. + if (XRE_IsContentProcess() && IsRoot()) { + dom::Document* doc = + const_cast<LocalAccessible*>(this)->AsDoc()->DocumentNode(); + dom::BrowsingContext* bc = doc->GetBrowsingContext(); + MOZ_ASSERT(bc); + if (!bc->Top()->IsInProcess()) { + return rel; + } + } + + // If accessible is in its own Window, or is the root of a document, + // then we should provide NODE_CHILD_OF relation so that MSAA clients + // can easily get to true parent instead of getting to oleacc's + // ROLE_WINDOW accessible which will prevent us from going up further + // (because it is system generated and has no idea about the hierarchy + // above it). + nsIFrame* frame = GetFrame(); + if (frame) { + nsView* view = frame->GetView(); + if (view) { + nsIScrollableFrame* scrollFrame = do_QueryFrame(frame); + if (scrollFrame || view->GetWidget() || !frame->GetParent()) { + rel.AppendTarget(LocalParent()); + } + } + } + + return rel; + } + + case RelationType::NODE_PARENT_OF: { + // ARIA tree or treegrid can do the hierarchy by @aria-level, ARIA trees + // also can be organized by groups. + if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM || + roleMapEntry->role == roles::LISTITEM || + roleMapEntry->role == roles::ROW || + roleMapEntry->role == roles::OUTLINE || + roleMapEntry->role == roles::LIST || + roleMapEntry->role == roles::TREE_TABLE)) { + return Relation(new ItemIterator(this)); + } + + return Relation(); + } + + case RelationType::CONTROLLED_BY: + return Relation(new RelatedAccIterator(Document(), mContent, + nsGkAtoms::aria_controls)); + + case RelationType::CONTROLLER_FOR: { + Relation rel( + new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_controls)); + rel.AppendIter(new HTMLOutputIterator(Document(), mContent)); + return rel; + } + + case RelationType::FLOWS_TO: + return Relation( + new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_flowto)); + + case RelationType::FLOWS_FROM: + return Relation( + new RelatedAccIterator(Document(), mContent, nsGkAtoms::aria_flowto)); + + case RelationType::MEMBER_OF: { + if (Role() == roles::RADIOBUTTON) { + /* If we see a radio button role here, we're dealing with an aria + * radio button (because input=radio buttons are + * HTMLRadioButtonAccessibles) */ + Relation rel = Relation(); + LocalAccessible* currParent = LocalParent(); + while (currParent && currParent->Role() != roles::RADIO_GROUP) { + currParent = currParent->LocalParent(); + } + + if (currParent && currParent->Role() == roles::RADIO_GROUP) { + /* If we found a radiogroup parent, search for all + * roles::RADIOBUTTON children and add them to our relation. + * This search will include the radio button this method + * was called from, which is expected. */ + Pivot p = Pivot(currParent); + PivotRoleRule rule(roles::RADIOBUTTON); + Accessible* match = p.Next(currParent, rule); + while (match) { + MOZ_ASSERT(match->IsLocal(), + "We shouldn't find any remote accs while building our " + "relation!"); + rel.AppendTarget(match->AsLocal()); + match = p.Next(match, rule); + } + } + + /* By webkit's standard, aria radio buttons do not get grouped + * if they lack a group parent, so we return an empty + * relation here if the above check fails. */ + + return rel; + } + + return Relation(mDoc, GetAtomicRegion()); + } + + case RelationType::LINKS_TO: { + Relation rel = Relation(); + if (Role() == roles::LINK) { + dom::HTMLAnchorElement* anchor = + dom::HTMLAnchorElement::FromNode(mContent); + if (!anchor) { + return rel; + } + // If this node is an anchor element, query its hash to find the + // target. + nsAutoString hash; + anchor->GetHash(hash); + if (hash.IsEmpty()) { + return rel; + } + + // GetHash returns an ID or name with a leading '#', trim it so we can + // search the doc by ID or name alone. + hash.Trim("#"); + if (dom::Element* elm = mContent->OwnerDoc()->GetElementById(hash)) { + rel.AppendTarget(mDoc->GetAccessibleOrContainer(elm)); + } else if (nsCOMPtr<nsINodeList> list = + mContent->OwnerDoc()->GetElementsByName(hash)) { + // Loop through the named nodes looking for the first anchor + uint32_t length = list->Length(); + for (uint32_t i = 0; i < length; i++) { + nsIContent* node = list->Item(i); + if (node->IsHTMLElement(nsGkAtoms::a)) { + rel.AppendTarget(mDoc->GetAccessibleOrContainer(node)); + break; + } + } + } + } + + return rel; + } + + case RelationType::SUBWINDOW_OF: + case RelationType::EMBEDS: + case RelationType::EMBEDDED_BY: + case RelationType::POPUP_FOR: + case RelationType::PARENT_WINDOW_OF: + return Relation(); + + case RelationType::DEFAULT_BUTTON: { + if (mContent->IsHTMLElement()) { + // HTML form controls implements nsIFormControl interface. + nsCOMPtr<nsIFormControl> control(do_QueryInterface(mContent)); + if (control) { + if (dom::HTMLFormElement* form = control->GetForm()) { + return Relation(mDoc, form->GetDefaultSubmitElement()); + } + } + } else { + // In XUL, use first <button default="true" .../> in the document + dom::Document* doc = mContent->OwnerDoc(); + nsIContent* buttonEl = nullptr; + if (doc->AllowXULXBL()) { + nsCOMPtr<nsIHTMLCollection> possibleDefaultButtons = + doc->GetElementsByAttribute(u"default"_ns, u"true"_ns); + if (possibleDefaultButtons) { + uint32_t length = possibleDefaultButtons->Length(); + // Check for button in list of default="true" elements + for (uint32_t count = 0; count < length && !buttonEl; count++) { + nsIContent* item = possibleDefaultButtons->Item(count); + RefPtr<nsIDOMXULButtonElement> button = + item->IsElement() ? item->AsElement()->AsXULButton() + : nullptr; + if (button) { + buttonEl = item; + } + } + } + return Relation(mDoc, buttonEl); + } + } + return Relation(); + } + + case RelationType::CONTAINING_DOCUMENT: + return Relation(mDoc); + + case RelationType::CONTAINING_TAB_PANE: { + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode()); + if (docShell) { + // Walk up the parent chain without crossing the boundary at which item + // types change, preventing us from walking up out of tab content. + nsCOMPtr<nsIDocShellTreeItem> root; + docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(root)); + if (root) { + // If the item type is typeContent, we assume we are in browser tab + // content. Note, this includes content such as about:addons, + // for consistency. + if (root->ItemType() == nsIDocShellTreeItem::typeContent) { + return Relation(nsAccUtils::GetDocAccessibleFor(root)); + } + } + } + return Relation(); + } + + case RelationType::CONTAINING_APPLICATION: + return Relation(ApplicationAcc()); + + case RelationType::DETAILS: + return Relation( + new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_details)); + + case RelationType::DETAILS_FOR: + return Relation( + new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_details)); + + case RelationType::ERRORMSG: + return Relation( + new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_errormessage)); + + case RelationType::ERRORMSG_FOR: + return Relation( + new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_errormessage)); + + default: + return Relation(); + } +} + +void LocalAccessible::GetNativeInterface(void** aNativeAccessible) {} + +void LocalAccessible::DoCommand(nsIContent* aContent, + uint32_t aActionIndex) const { + class Runnable final : public mozilla::Runnable { + public: + Runnable(const LocalAccessible* aAcc, nsIContent* aContent, uint32_t aIdx) + : mozilla::Runnable("Runnable"), + mAcc(aAcc), + mContent(aContent), + mIdx(aIdx) {} + + // XXX Cannot mark as MOZ_CAN_RUN_SCRIPT because the base class change + // requires too big changes across a lot of modules. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + if (mAcc) { + MOZ_KnownLive(mAcc)->DispatchClickEvent(MOZ_KnownLive(mContent), mIdx); + } + return NS_OK; + } + + void Revoke() { + mAcc = nullptr; + mContent = nullptr; + } + + private: + RefPtr<const LocalAccessible> mAcc; + nsCOMPtr<nsIContent> mContent; + uint32_t mIdx; + }; + + nsIContent* content = aContent ? aContent : mContent.get(); + nsCOMPtr<nsIRunnable> runnable = new Runnable(this, content, aActionIndex); + NS_DispatchToMainThread(runnable); +} + +void LocalAccessible::DispatchClickEvent(nsIContent* aContent, + uint32_t aActionIndex) const { + if (IsDefunct()) return; + + RefPtr<PresShell> presShell = mDoc->PresShellPtr(); + + // Scroll into view. + presShell->ScrollContentIntoView(aContent, ScrollAxis(), ScrollAxis(), + ScrollFlags::ScrollOverflowHidden); + + AutoWeakFrame frame = aContent->GetPrimaryFrame(); + if (!frame) return; + + // Compute x and y coordinates. + nsPoint point; + nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point); + if (!widget) return; + + nsSize size = frame->GetSize(); + + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + int32_t x = presContext->AppUnitsToDevPixels(point.x + size.width / 2); + int32_t y = presContext->AppUnitsToDevPixels(point.y + size.height / 2); + + // Simulate a touch interaction by dispatching touch events with mouse events. + nsCoreUtils::DispatchTouchEvent(eTouchStart, x, y, aContent, frame, presShell, + widget); + nsCoreUtils::DispatchMouseEvent(eMouseDown, x, y, aContent, frame, presShell, + widget); + nsCoreUtils::DispatchTouchEvent(eTouchEnd, x, y, aContent, frame, presShell, + widget); + nsCoreUtils::DispatchMouseEvent(eMouseUp, x, y, aContent, frame, presShell, + widget); +} + +void LocalAccessible::ScrollToPoint(uint32_t aCoordinateType, int32_t aX, + int32_t aY) { + nsIFrame* frame = GetFrame(); + if (!frame) return; + + LayoutDeviceIntPoint coords = + nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this); + + nsIFrame* parentFrame = frame; + while ((parentFrame = parentFrame->GetParent())) { + nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords); + } +} + +void LocalAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset, + uint32_t aLength) { + // Return text representation of non-text accessible within hypertext + // accessible. Text accessible overrides this method to return enclosed text. + if (aStartOffset != 0 || aLength == 0) return; + + MOZ_ASSERT(mParent, + "Called on accessible unbound from tree. Result can be wrong."); + nsIFrame* frame = GetFrame(); + // We handle something becoming display: none async, which means we won't have + // a frame when we're queuing text removed events. Thus, it's important that + // we produce text here even if there's no frame. Otherwise, we won't fire a + // text removed event at all, which might leave client caches (e.g. NVDA + // virtual buffers) with dead nodes. + if (IsHTMLBr() || (frame && frame->IsBrFrame())) { + aText += kForcedNewLineChar; + } else if (mParent && nsAccUtils::MustPrune(mParent)) { + // Expose the embedded object accessible as imaginary embedded object + // character if its parent hypertext accessible doesn't expose children to + // AT. + aText += kImaginaryEmbeddedObjectChar; + } else { + aText += kEmbeddedObjectChar; + } +} + +void LocalAccessible::Shutdown() { + // Mark the accessible as defunct, invalidate the child count and pointers to + // other accessibles, also make sure none of its children point to this + // parent + mStateFlags |= eIsDefunct; + + int32_t childCount = mChildren.Length(); + for (int32_t childIdx = 0; childIdx < childCount; childIdx++) { + mChildren.ElementAt(childIdx)->UnbindFromParent(); + } + mChildren.Clear(); + + mEmbeddedObjCollector = nullptr; + + if (mParent) mParent->RemoveChild(this); + + mContent = nullptr; + mDoc = nullptr; + if (SelectionMgr() && SelectionMgr()->AccessibleWithCaret(nullptr) == this) { + SelectionMgr()->ResetCaretOffset(); + } +} + +// LocalAccessible protected +void LocalAccessible::ARIAName(nsString& aName) const { + // aria-labelledby now takes precedence over aria-label + nsresult rv = nsTextEquivUtils::GetTextEquivFromIDRefs( + this, nsGkAtoms::aria_labelledby, aName); + if (NS_SUCCEEDED(rv)) { + aName.CompressWhitespace(); + } + + if (aName.IsEmpty() && mContent->IsElement() && + nsAccUtils::GetARIAAttr(mContent->AsElement(), nsGkAtoms::aria_label, + aName)) { + aName.CompressWhitespace(); + } +} + +// LocalAccessible protected +void LocalAccessible::ARIADescription(nsString& aDescription) const { + // aria-describedby takes precedence over aria-description + nsresult rv = nsTextEquivUtils::GetTextEquivFromIDRefs( + this, nsGkAtoms::aria_describedby, aDescription); + if (NS_SUCCEEDED(rv)) { + aDescription.CompressWhitespace(); + } + + if (aDescription.IsEmpty() && mContent->IsElement() && + nsAccUtils::GetARIAAttr(mContent->AsElement(), + nsGkAtoms::aria_description, aDescription)) { + aDescription.CompressWhitespace(); + } +} + +// LocalAccessible protected +ENameValueFlag LocalAccessible::NativeName(nsString& aName) const { + if (mContent->IsHTMLElement()) { + LocalAccessible* label = nullptr; + HTMLLabelIterator iter(Document(), this); + while ((label = iter.Next())) { + nsTextEquivUtils::AppendTextEquivFromContent(this, label->GetContent(), + &aName); + aName.CompressWhitespace(); + } + + if (!aName.IsEmpty()) return eNameOK; + + NameFromAssociatedXULLabel(mDoc, mContent, aName); + if (!aName.IsEmpty()) { + return eNameOK; + } + + nsTextEquivUtils::GetNameFromSubtree(this, aName); + return aName.IsEmpty() ? eNameOK : eNameFromSubtree; + } + + if (mContent->IsXULElement()) { + XULElmName(mDoc, mContent, aName); + if (!aName.IsEmpty()) return eNameOK; + + nsTextEquivUtils::GetNameFromSubtree(this, aName); + return aName.IsEmpty() ? eNameOK : eNameFromSubtree; + } + + if (mContent->IsSVGElement()) { + // If user agents need to choose among multiple 'desc' or 'title' + // elements for processing, the user agent shall choose the first one. + for (nsIContent* childElm = mContent->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (childElm->IsSVGElement(nsGkAtoms::title)) { + nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName); + return eNameOK; + } + } + } + + return eNameOK; +} + +// LocalAccessible protected +void LocalAccessible::NativeDescription(nsString& aDescription) const { + bool isXUL = mContent->IsXULElement(); + if (isXUL) { + // Try XUL <description control="[id]">description text</description> + XULDescriptionIterator iter(Document(), mContent); + LocalAccessible* descr = nullptr; + while ((descr = iter.Next())) { + nsTextEquivUtils::AppendTextEquivFromContent(this, descr->GetContent(), + &aDescription); + } + } +} + +// LocalAccessible protected +void LocalAccessible::BindToParent(LocalAccessible* aParent, + uint32_t aIndexInParent) { + MOZ_ASSERT(aParent, "This method isn't used to set null parent"); + MOZ_ASSERT(!mParent, "The child was expected to be moved"); + +#ifdef A11Y_LOG + if (mParent) { + logging::TreeInfo("BindToParent: stealing accessible", 0, "old parent", + mParent, "new parent", aParent, "child", this, nullptr); + } +#endif + + mParent = aParent; + mIndexInParent = aIndexInParent; + + if (mParent->HasNameDependent() || mParent->IsXULListItem() || + RelationByType(RelationType::LABEL_FOR).Next() || + nsTextEquivUtils::HasNameRule(mParent, eNameFromSubtreeRule)) { + mContextFlags |= eHasNameDependent; + } else { + mContextFlags &= ~eHasNameDependent; + } + if (mParent->HasDescriptionDependent() || + RelationByType(RelationType::DESCRIPTION_FOR).Next()) { + mContextFlags |= eHasDescriptionDependent; + } else { + mContextFlags &= ~eHasDescriptionDependent; + } + + // Add name/description dependent flags for dependent content once + // a name/description provider is added to doc. + Relation rel = RelationByType(RelationType::LABELLED_BY); + LocalAccessible* relTarget = nullptr; + while ((relTarget = rel.LocalNext())) { + if (!relTarget->HasNameDependent()) { + relTarget->ModifySubtreeContextFlags(eHasNameDependent, true); + } + } + + rel = RelationByType(RelationType::DESCRIBED_BY); + while ((relTarget = rel.LocalNext())) { + if (!relTarget->HasDescriptionDependent()) { + relTarget->ModifySubtreeContextFlags(eHasDescriptionDependent, true); + } + } + + mContextFlags |= + static_cast<uint32_t>((mParent->IsAlert() || mParent->IsInsideAlert())) & + eInsideAlert; + + if (TableCellAccessible* cell = AsTableCell()) { + if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { + CachedTableAccessible::Invalidate(this); + } else if (Role() == roles::COLUMNHEADER) { + // A new column header is being added. Invalidate the table's header + // cache. + TableAccessible* table = cell->Table(); + if (table) { + table->GetHeaderCache().Clear(); + } + } + } +} + +// LocalAccessible protected +void LocalAccessible::UnbindFromParent() { + // We do this here to handle document shutdown and an Accessible being moved. + // We do this for subtree removal in DocAccessible::UncacheChildrenInSubtree. + if (StaticPrefs::accessibility_cache_enabled_AtStartup() && + (IsTable() || IsTableCell())) { + CachedTableAccessible::Invalidate(this); + } + + mParent = nullptr; + mIndexInParent = -1; + mIndexOfEmbeddedChild = -1; + + delete mGroupInfo; + mGroupInfo = nullptr; + mContextFlags &= ~eHasNameDependent & ~eInsideAlert; +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible public methods + +RootAccessible* LocalAccessible::RootAccessible() const { + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode()); + NS_ASSERTION(docShell, "No docshell for mContent"); + if (!docShell) { + return nullptr; + } + + nsCOMPtr<nsIDocShellTreeItem> root; + docShell->GetInProcessRootTreeItem(getter_AddRefs(root)); + NS_ASSERTION(root, "No root content tree item"); + if (!root) { + return nullptr; + } + + DocAccessible* docAcc = nsAccUtils::GetDocAccessibleFor(root); + return docAcc ? docAcc->AsRoot() : nullptr; +} + +nsIFrame* LocalAccessible::GetFrame() const { + return mContent ? mContent->GetPrimaryFrame() : nullptr; +} + +nsINode* LocalAccessible::GetNode() const { return mContent; } + +dom::Element* LocalAccessible::Elm() const { + return dom::Element::FromNodeOrNull(mContent); +} + +void LocalAccessible::Language(nsAString& aLanguage) { + aLanguage.Truncate(); + + if (!mDoc) return; + + nsCoreUtils::GetLanguageFor(mContent, nullptr, aLanguage); + if (aLanguage.IsEmpty()) { // Nothing found, so use document's language + mDoc->DocumentNode()->GetHeaderData(nsGkAtoms::headerContentLanguage, + aLanguage); + } +} + +bool LocalAccessible::InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) { + if (!aChild) return false; + + if (aIndex == mChildren.Length()) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mChildren.AppendElement(aChild); + } else { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mChildren.InsertElementAt(aIndex, aChild); + + MOZ_ASSERT(mStateFlags & eKidsMutating, "Illicit children change"); + + for (uint32_t idx = aIndex + 1; idx < mChildren.Length(); idx++) { + mChildren[idx]->mIndexInParent = idx; + } + } + + if (aChild->IsText()) { + mStateFlags |= eHasTextKids; + } + + aChild->BindToParent(this, aIndex); + return true; +} + +bool LocalAccessible::RemoveChild(LocalAccessible* aChild) { + MOZ_DIAGNOSTIC_ASSERT(aChild, "No child was given"); + MOZ_DIAGNOSTIC_ASSERT(aChild->mParent, "No parent"); + MOZ_DIAGNOSTIC_ASSERT(aChild->mParent == this, "Wrong parent"); + MOZ_DIAGNOSTIC_ASSERT(aChild->mIndexInParent != -1, + "Unbound child was given"); + MOZ_DIAGNOSTIC_ASSERT((mStateFlags & eKidsMutating) || aChild->IsDefunct() || + aChild->IsDoc() || IsApplication(), + "Illicit children change"); + + int32_t index = static_cast<uint32_t>(aChild->mIndexInParent); + if (mChildren.SafeElementAt(index) != aChild) { + MOZ_ASSERT_UNREACHABLE("A wrong child index"); + index = mChildren.IndexOf(aChild); + if (index == -1) { + MOZ_ASSERT_UNREACHABLE("No child was found"); + return false; + } + } + + aChild->UnbindFromParent(); + mChildren.RemoveElementAt(index); + + for (uint32_t idx = index; idx < mChildren.Length(); idx++) { + mChildren[idx]->mIndexInParent = idx; + } + + return true; +} + +void LocalAccessible::RelocateChild(uint32_t aNewIndex, + LocalAccessible* aChild) { + MOZ_DIAGNOSTIC_ASSERT(aChild, "No child was given"); + MOZ_DIAGNOSTIC_ASSERT(aChild->mParent == this, + "A child from different subtree was given"); + MOZ_DIAGNOSTIC_ASSERT(aChild->mIndexInParent != -1, + "Unbound child was given"); + MOZ_DIAGNOSTIC_ASSERT( + aChild->mParent->LocalChildAt(aChild->mIndexInParent) == aChild, + "Wrong index in parent"); + MOZ_DIAGNOSTIC_ASSERT( + static_cast<uint32_t>(aChild->mIndexInParent) != aNewIndex, + "No move, same index"); + MOZ_DIAGNOSTIC_ASSERT(aNewIndex <= mChildren.Length(), + "Wrong new index was given"); + + RefPtr<AccHideEvent> hideEvent = new AccHideEvent(aChild, false); + if (mDoc->Controller()->QueueMutationEvent(hideEvent)) { + aChild->SetHideEventTarget(true); + } + + mEmbeddedObjCollector = nullptr; + mChildren.RemoveElementAt(aChild->mIndexInParent); + + uint32_t startIdx = aNewIndex, endIdx = aChild->mIndexInParent; + + // If the child is moved after its current position. + if (static_cast<uint32_t>(aChild->mIndexInParent) < aNewIndex) { + startIdx = aChild->mIndexInParent; + if (aNewIndex == mChildren.Length() + 1) { + // The child is moved to the end. + mChildren.AppendElement(aChild); + endIdx = mChildren.Length() - 1; + } else { + mChildren.InsertElementAt(aNewIndex - 1, aChild); + endIdx = aNewIndex; + } + } else { + // The child is moved prior its current position. + mChildren.InsertElementAt(aNewIndex, aChild); + } + + for (uint32_t idx = startIdx; idx <= endIdx; idx++) { + mChildren[idx]->mIndexInParent = idx; + mChildren[idx]->mIndexOfEmbeddedChild = -1; + } + + for (uint32_t idx = 0; idx < mChildren.Length(); idx++) { + mChildren[idx]->mStateFlags |= eGroupInfoDirty; + } + + RefPtr<AccShowEvent> showEvent = new AccShowEvent(aChild); + DebugOnly<bool> added = mDoc->Controller()->QueueMutationEvent(showEvent); + MOZ_ASSERT(added); + aChild->SetShowEventTarget(true); +} + +LocalAccessible* LocalAccessible::LocalChildAt(uint32_t aIndex) const { + LocalAccessible* child = mChildren.SafeElementAt(aIndex, nullptr); + if (!child) return nullptr; + +#ifdef DEBUG + LocalAccessible* realParent = child->mParent; + NS_ASSERTION(!realParent || realParent == this, + "Two accessibles have the same first child accessible!"); +#endif + + return child; +} + +uint32_t LocalAccessible::ChildCount() const { return mChildren.Length(); } + +int32_t LocalAccessible::IndexInParent() const { return mIndexInParent; } + +uint32_t LocalAccessible::EmbeddedChildCount() { + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) { + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + } + return mEmbeddedObjCollector->Count(); + } + + return ChildCount(); +} + +LocalAccessible* LocalAccessible::EmbeddedChildAt(uint32_t aIndex) { + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) { + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + } + return mEmbeddedObjCollector.get() + ? mEmbeddedObjCollector->GetAccessibleAt(aIndex) + : nullptr; + } + + return LocalChildAt(aIndex); +} + +int32_t LocalAccessible::IndexOfEmbeddedChild(Accessible* aChild) { + MOZ_ASSERT(aChild->IsLocal()); + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) { + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + } + return mEmbeddedObjCollector.get() + ? mEmbeddedObjCollector->GetIndexAt(aChild->AsLocal()) + : -1; + } + + return GetIndexOf(aChild->AsLocal()); +} + +//////////////////////////////////////////////////////////////////////////////// +// HyperLinkAccessible methods + +bool LocalAccessible::IsLink() const { + // Every embedded accessible within hypertext accessible implements + // hyperlink interface. + return mParent && mParent->IsHyperText() && !IsText(); +} + +uint32_t LocalAccessible::AnchorCount() { + MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!"); + return 1; +} + +LocalAccessible* LocalAccessible::AnchorAt(uint32_t aAnchorIndex) { + MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!"); + return aAnchorIndex == 0 ? this : nullptr; +} + +already_AddRefed<nsIURI> LocalAccessible::AnchorURIAt( + uint32_t aAnchorIndex) const { + MOZ_ASSERT(IsLink(), "AnchorURIAt is called on not hyper link!"); + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// SelectAccessible + +void LocalAccessible::SelectedItems(nsTArray<Accessible*>* aItems) { + AccIterator iter(this, filters::GetSelected); + LocalAccessible* selected = nullptr; + while ((selected = iter.Next())) aItems->AppendElement(selected); +} + +uint32_t LocalAccessible::SelectedItemCount() { + uint32_t count = 0; + AccIterator iter(this, filters::GetSelected); + LocalAccessible* selected = nullptr; + while ((selected = iter.Next())) ++count; + + return count; +} + +Accessible* LocalAccessible::GetSelectedItem(uint32_t aIndex) { + AccIterator iter(this, filters::GetSelected); + LocalAccessible* selected = nullptr; + + uint32_t index = 0; + while ((selected = iter.Next()) && index < aIndex) index++; + + return selected; +} + +bool LocalAccessible::IsItemSelected(uint32_t aIndex) { + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + LocalAccessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) index++; + + return selected && selected->State() & states::SELECTED; +} + +bool LocalAccessible::AddItemToSelection(uint32_t aIndex) { + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + LocalAccessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) index++; + + if (selected) selected->SetSelected(true); + + return static_cast<bool>(selected); +} + +bool LocalAccessible::RemoveItemFromSelection(uint32_t aIndex) { + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + LocalAccessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) index++; + + if (selected) selected->SetSelected(false); + + return static_cast<bool>(selected); +} + +bool LocalAccessible::SelectAll() { + bool success = false; + LocalAccessible* selectable = nullptr; + + AccIterator iter(this, filters::GetSelectable); + while ((selectable = iter.Next())) { + success = true; + selectable->SetSelected(true); + } + return success; +} + +bool LocalAccessible::UnselectAll() { + bool success = false; + LocalAccessible* selected = nullptr; + + AccIterator iter(this, filters::GetSelected); + while ((selected = iter.Next())) { + success = true; + selected->SetSelected(false); + } + return success; +} + +//////////////////////////////////////////////////////////////////////////////// +// Widgets + +bool LocalAccessible::IsWidget() const { return false; } + +bool LocalAccessible::IsActiveWidget() const { + if (FocusMgr()->HasDOMFocus(mContent)) return true; + + // If text entry of combobox widget has a focus then the combobox widget is + // active. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::combobox)) { + uint32_t childCount = ChildCount(); + for (uint32_t idx = 0; idx < childCount; idx++) { + LocalAccessible* child = mChildren.ElementAt(idx); + if (child->Role() == roles::ENTRY) { + return FocusMgr()->HasDOMFocus(child->GetContent()); + } + } + } + + return false; +} + +bool LocalAccessible::AreItemsOperable() const { + return HasOwnContent() && mContent->IsElement() && + mContent->AsElement()->HasAttr(nsGkAtoms::aria_activedescendant); +} + +LocalAccessible* LocalAccessible::CurrentItem() const { + // Check for aria-activedescendant, which changes which element has focus. + // For activedescendant, the ARIA spec does not require that the user agent + // checks whether pointed node is actually a DOM descendant of the element + // with the aria-activedescendant attribute. + nsAutoString id; + if (HasOwnContent() && mContent->IsElement() && + mContent->AsElement()->GetAttr(nsGkAtoms::aria_activedescendant, id)) { + dom::Element* activeDescendantElm = IDRefsIterator::GetElem(mContent, id); + if (activeDescendantElm) { + if (mContent->IsInclusiveDescendantOf(activeDescendantElm)) { + // Don't want a cyclical descendant relationship. That would be bad. + return nullptr; + } + + DocAccessible* document = Document(); + if (document) return document->GetAccessible(activeDescendantElm); + } + } + return nullptr; +} + +void LocalAccessible::SetCurrentItem(const LocalAccessible* aItem) { + nsAtom* id = aItem->GetContent()->GetID(); + if (id) { + nsAutoString idStr; + id->ToString(idStr); + mContent->AsElement()->SetAttr( + kNameSpaceID_None, nsGkAtoms::aria_activedescendant, idStr, true); + } +} + +LocalAccessible* LocalAccessible::ContainerWidget() const { + if (HasARIARole() && mContent->HasID()) { + for (LocalAccessible* parent = LocalParent(); parent; + parent = parent->LocalParent()) { + nsIContent* parentContent = parent->GetContent(); + if (parentContent && parentContent->IsElement() && + parentContent->AsElement()->HasAttr( + nsGkAtoms::aria_activedescendant)) { + return parent; + } + + // Don't cross DOM document boundaries. + if (parent->IsDoc()) break; + } + } + return nullptr; +} + +bool LocalAccessible::IsActiveDescendant(LocalAccessible** aWidget) const { + if (!HasOwnContent() || !mContent->HasID()) { + return false; + } + + dom::DocumentOrShadowRoot* docOrShadowRoot = + mContent->GetUncomposedDocOrConnectedShadowRoot(); + if (!docOrShadowRoot) { + return false; + } + + nsAutoCString selector; + selector.AppendPrintf( + "[aria-activedescendant=\"%s\"]", + NS_ConvertUTF16toUTF8(mContent->GetID()->GetUTF16String()).get()); + IgnoredErrorResult er; + + dom::Element* widgetElm = + docOrShadowRoot->AsNode().QuerySelector(selector, er); + + if (!widgetElm || er.Failed()) { + return false; + } + + if (widgetElm->IsInclusiveDescendantOf(mContent)) { + // Don't want a cyclical descendant relationship. That would be bad. + return false; + } + + LocalAccessible* widget = mDoc->GetAccessible(widgetElm); + + if (aWidget) { + *aWidget = widget; + } + + return !!widget; +} + +void LocalAccessible::Announce(const nsAString& aAnnouncement, + uint16_t aPriority) { + RefPtr<AccAnnouncementEvent> event = + new AccAnnouncementEvent(this, aAnnouncement, aPriority); + nsEventShell::FireEvent(event); +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible protected methods + +void LocalAccessible::LastRelease() { + // First cleanup if needed... + if (mDoc) { + Shutdown(); + NS_ASSERTION(!mDoc, + "A Shutdown() impl forgot to call its parent's Shutdown?"); + } + // ... then die. + delete this; +} + +LocalAccessible* LocalAccessible::GetSiblingAtOffset(int32_t aOffset, + nsresult* aError) const { + if (!mParent || mIndexInParent == -1) { + if (aError) *aError = NS_ERROR_UNEXPECTED; + + return nullptr; + } + + if (aError && + mIndexInParent + aOffset >= static_cast<int32_t>(mParent->ChildCount())) { + *aError = NS_OK; // fail peacefully + return nullptr; + } + + LocalAccessible* child = mParent->LocalChildAt(mIndexInParent + aOffset); + if (aError && !child) *aError = NS_ERROR_UNEXPECTED; + + return child; +} + +void LocalAccessible::ModifySubtreeContextFlags(uint32_t aContextFlags, + bool aAdd) { + Pivot pivot(this); + LocalAccInSameDocRule rule; + for (Accessible* anchor = this; anchor; anchor = pivot.Next(anchor, rule)) { + MOZ_ASSERT(anchor->IsLocal()); + LocalAccessible* acc = anchor->AsLocal(); + if (aAdd) { + acc->mContextFlags |= aContextFlags; + } else { + acc->mContextFlags &= ~aContextFlags; + } + } +} + +double LocalAccessible::AttrNumericValue(nsAtom* aAttr) const { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) { + return UnspecifiedNaN<double>(); + } + + nsAutoString attrValue; + if (!mContent->IsElement() || + !nsAccUtils::GetARIAAttr(mContent->AsElement(), aAttr, attrValue)) { + return UnspecifiedNaN<double>(); + } + + nsresult error = NS_OK; + double value = attrValue.ToDouble(&error); + return NS_FAILED(error) ? UnspecifiedNaN<double>() : value; +} + +uint32_t LocalAccessible::GetActionRule() const { + if (!HasOwnContent() || (InteractiveState() & states::UNAVAILABLE)) { + return eNoAction; + } + + // Return "click" action on elements that have an attached popup menu. + if (mContent->IsXULElement()) { + if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::popup)) { + return eClickAction; + } + } + + // Has registered 'click' event handler. + bool isOnclick = nsCoreUtils::HasClickListener(mContent); + + if (isOnclick) return eClickAction; + + // Get an action based on ARIA role. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && roleMapEntry->actionRule != eNoAction) { + return roleMapEntry->actionRule; + } + + // Get an action based on ARIA attribute. + if (nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_expanded)) { + return eExpandAction; + } + + return eNoAction; +} + +AccGroupInfo* LocalAccessible::GetGroupInfo() const { + if (mGroupInfo && !(mStateFlags & eGroupInfoDirty)) { + return mGroupInfo; + } + + return nullptr; +} + +AccGroupInfo* LocalAccessible::GetOrCreateGroupInfo() { + if (mGroupInfo) { + if (mStateFlags & eGroupInfoDirty) { + mGroupInfo->Update(); + mStateFlags &= ~eGroupInfoDirty; + } + + return mGroupInfo; + } + + mGroupInfo = AccGroupInfo::CreateGroupInfo(this); + mStateFlags &= ~eGroupInfoDirty; + return mGroupInfo; +} + +void LocalAccessible::SendCache(uint64_t aCacheDomain, + CacheUpdateType aUpdateType) { + if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) { + return; + } + + if (!IPCAccessibilityActive() || !Document()) { + return; + } + + DocAccessibleChild* ipcDoc = mDoc->IPCDoc(); + if (!ipcDoc) { + // This means DocAccessible::DoInitialUpdate hasn't been called yet, which + // means the a11y tree hasn't been built yet. Therefore, this should only + // be possible if this is a DocAccessible. + MOZ_ASSERT(IsDoc(), "Called on a non-DocAccessible but IPCDoc is null"); + return; + } + + RefPtr<AccAttributes> fields = + BundleFieldsForCache(aCacheDomain, aUpdateType); + if (!fields->Count()) { + return; + } + nsTArray<CacheData> data; + data.AppendElement(CacheData(ID(), fields)); + ipcDoc->SendCache(aUpdateType, data, false); + + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString updateTypeStr; + if (aUpdateType == CacheUpdateType::Initial) { + updateTypeStr = "Initial"; + } else if (aUpdateType == CacheUpdateType::Update) { + updateTypeStr = "Update"; + } else { + updateTypeStr = "Other"; + } + PROFILER_MARKER_TEXT("LocalAccessible::SendCache", A11Y, {}, updateTypeStr); + } +} + +already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache( + uint64_t aCacheDomain, CacheUpdateType aUpdateType) { + RefPtr<AccAttributes> fields = new AccAttributes(); + + // Caching name for text leaf Accessibles is redundant, since their name is + // always their text. Text gets handled below. + if (aCacheDomain & CacheDomain::NameAndDescription && !IsText()) { + nsString name; + int32_t nameFlag = Name(name); + if (nameFlag != eNameOK) { + fields->SetAttribute(nsGkAtoms::explicit_name, nameFlag); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::explicit_name, DeleteEntry()); + } + + if (!name.IsEmpty()) { + fields->SetAttribute(nsGkAtoms::name, std::move(name)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::name, DeleteEntry()); + } + + nsString description; + Description(description); + if (!description.IsEmpty()) { + fields->SetAttribute(nsGkAtoms::description, std::move(description)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::description, DeleteEntry()); + } + } + + if (aCacheDomain & CacheDomain::Value) { + // We cache the text value in 3 cases: + // 1. Accessible is an HTML input type that holds a number. + // 2. Accessible has a numeric value and an aria-valuetext. + // 3. Accessible is an HTML input type that holds text. + // 4. Accessible is a link, in which case value is the target URL. + // ... for all other cases we divine the value remotely. + bool cacheValueText = false; + if (HasNumericValue()) { + fields->SetAttribute(nsGkAtoms::value, CurValue()); + fields->SetAttribute(nsGkAtoms::max, MaxValue()); + fields->SetAttribute(nsGkAtoms::min, MinValue()); + fields->SetAttribute(nsGkAtoms::step, Step()); + cacheValueText = NativeHasNumericValue() || + (mContent->IsElement() && + nsAccUtils::HasARIAAttr(mContent->AsElement(), + nsGkAtoms::aria_valuetext)); + } else { + cacheValueText = IsTextField() || IsHTMLLink(); + } + + if (cacheValueText) { + nsString value; + Value(value); + if (!value.IsEmpty()) { + fields->SetAttribute(nsGkAtoms::aria_valuetext, std::move(value)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::aria_valuetext, DeleteEntry()); + } + } + } + + if (aCacheDomain & CacheDomain::Viewport && IsDoc()) { + // Construct the viewport cache for this document. This cache domain will + // only be requested after we finish painting. + DocAccessible* doc = AsDoc(); + PresShell* presShell = doc->PresShellPtr(); + + if (nsIFrame* rootFrame = presShell->GetRootFrame()) { + nsTArray<nsIFrame*> frames; + nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable(); + nsRect scrollPort = sf ? sf->GetScrollPortRect() : rootFrame->GetRect(); + + nsLayoutUtils::GetFramesForArea( + RelativeTo{rootFrame}, scrollPort, frames, + {{// We only care about visible content for hittesting. + nsLayoutUtils::FrameForPointOption::OnlyVisible, + // This flag ensures the display lists are built, even if + // the page hasn't finished loading. + nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression, + // Each doc should have its own viewport cache, so we can + // ignore cross-doc content as an optimization. + nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc}}); + + nsTHashSet<LocalAccessible*> inViewAccs; + nsTArray<uint64_t> viewportCache; + // Layout considers table rows fully occluded by their containing cells. + // This means they don't have their own display list items, and they won't + // show up in the list returned from GetFramesForArea. To prevent table + // rows from appearing offscreen, we manually add any rows for which we + // have on-screen cells. + LocalAccessible* prevParentRow = nullptr; + for (nsIFrame* frame : frames) { + nsIContent* content = frame->GetContent(); + if (!content) { + continue; + } + + LocalAccessible* acc = doc->GetAccessible(content); + // The document should always be present at the end of the list, so + // including it is unnecessary and wasteful. We skip the document here + // and handle it as a fallback when hit testing. + if (!acc || acc == mDoc) { + continue; + } + + if (acc->IsTextLeaf() && nsAccUtils::MustPrune(acc->LocalParent())) { + acc = acc->LocalParent(); + } + if (acc->IsTableCell()) { + LocalAccessible* parent = acc->LocalParent(); + if (parent && parent->IsTableRow() && parent != prevParentRow) { + // If we've entered a new row since the last cell we saw, add the + // previous parent row to our viewport cache here to maintain + // hittesting order. Keep track of the current parent row. + if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) { + viewportCache.AppendElement(prevParentRow->ID()); + } + prevParentRow = parent; + } + } else if (acc->IsTable()) { + // If we've encountered a table, we know we've already + // handled all of this table's content (because we're traversing + // in hittesting order). Add our table's final row to the viewport + // cache before adding the table itself. Reset our marker for the next + // table. + if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) { + viewportCache.AppendElement(prevParentRow->ID()); + } + prevParentRow = nullptr; + } else if (acc->IsImageMap()) { + // Layout doesn't walk image maps, so we do that + // manually here. We do this before adding the map itself + // so the children come earlier in the hittesting order. + for (uint32_t i = 0; i < acc->ChildCount(); i++) { + LocalAccessible* child = acc->LocalChildAt(i); + MOZ_ASSERT(child); + if (inViewAccs.EnsureInserted(child)) { + MOZ_ASSERT(!child->IsDoc()); + viewportCache.AppendElement(child->ID()); + } + } + } else if (acc->IsHTMLCombobox()) { + // Layout doesn't consider combobox lists (or their + // currently selected items) to be onscreen, but we do. + // Add those things manually here. + HTMLComboboxAccessible* combobox = + static_cast<HTMLComboboxAccessible*>(acc); + HTMLComboboxListAccessible* list = combobox->List(); + LocalAccessible* currItem = combobox->SelectedOption(); + // Preserve hittesting order by adding the item, then + // the list, and finally the combobox itself. + if (currItem && inViewAccs.EnsureInserted(currItem)) { + viewportCache.AppendElement(currItem->ID()); + } + if (list && inViewAccs.EnsureInserted(list)) { + viewportCache.AppendElement(list->ID()); + } + } + + if (inViewAccs.EnsureInserted(acc)) { + MOZ_ASSERT(!acc->IsDoc()); + viewportCache.AppendElement(acc->ID()); + } + } + + if (viewportCache.Length()) { + fields->SetAttribute(nsGkAtoms::viewport, std::move(viewportCache)); + } + } + } + + bool boundsChanged = false; + if (aCacheDomain & CacheDomain::Bounds) { + nsRect newBoundsRect = ParentRelativeBounds(); + + // 1. Layout might notify us of a possible bounds change when the bounds + // haven't really changed. Therefore, we cache the last bounds we sent + // and don't send an update if they haven't changed. + // 2. For an initial cache push, we ignore 1) and always send the bounds. + // This handles the case where this LocalAccessible was moved (but not + // re-created). In that case, we will have cached bounds, but we currently + // do an initial cache push. + MOZ_ASSERT(aUpdateType == CacheUpdateType::Initial || mBounds.isSome(), + "Incremental cache push but mBounds is not set!"); + + if (OuterDocAccessible* doc = AsOuterDoc()) { + if (nsIFrame* docFrame = doc->GetFrame()) { + const nsMargin& newOffset = docFrame->GetUsedBorderAndPadding(); + Maybe<nsMargin> currOffset = doc->GetCrossDocOffset(); + if (!currOffset || *currOffset != newOffset) { + // OOP iframe docs can't compute their position within their + // cross-proc parent, so we have to manually cache that offset + // on the parent (outer doc) itself. For simplicity and consistency, + // we do this here for both OOP and in-process iframes. For in-process + // iframes, this also avoids the need to push a cache update for the + // embedded document when the iframe changes its padding, gets + // re-created, etc. Similar to bounds, we maintain a local cache and a + // remote cache to avoid sending redundant updates. + doc->SetCrossDocOffset(newOffset); + nsTArray<int32_t> offsetArray(2); + offsetArray.AppendElement(newOffset.Side(eSideLeft)); // X offset + offsetArray.AppendElement(newOffset.Side(eSideTop)); // Y offset + fields->SetAttribute(nsGkAtoms::crossorigin, std::move(offsetArray)); + } + } + } + + boundsChanged = aUpdateType == CacheUpdateType::Initial || + !newBoundsRect.IsEqualEdges(mBounds.value()); + if (boundsChanged) { + mBounds = Some(newBoundsRect); + + nsTArray<int32_t> boundsArray(4); + + boundsArray.AppendElement(newBoundsRect.x); + boundsArray.AppendElement(newBoundsRect.y); + boundsArray.AppendElement(newBoundsRect.width); + boundsArray.AppendElement(newBoundsRect.height); + + fields->SetAttribute(nsGkAtoms::relativeBounds, std::move(boundsArray)); + } + } + + if (aCacheDomain & CacheDomain::Text) { + if (!HasChildren()) { + // We only cache text and line offsets on leaf Accessibles. + // Only text Accessibles can have actual text. + if (IsText()) { + nsString text; + AppendTextTo(text); + fields->SetAttribute(nsGkAtoms::text, std::move(text)); + TextLeafPoint point(this, 0); + RefPtr<AccAttributes> attrs = point.GetTextAttributesLocalAcc( + /* aIncludeDefaults */ false); + fields->SetAttribute(nsGkAtoms::style, std::move(attrs)); + } + } + if (HyperTextAccessible* ht = AsHyperText()) { + RefPtr<AccAttributes> attrs = ht->DefaultTextAttributes(); + fields->SetAttribute(nsGkAtoms::style, std::move(attrs)); + } + } + + // If text changes, we must also update spelling errors. + if (aCacheDomain & (CacheDomain::Spelling | CacheDomain::Text) && + IsTextLeaf()) { + auto spellingErrors = TextLeafPoint::GetSpellingErrorOffsets(this); + if (!spellingErrors.IsEmpty()) { + fields->SetAttribute(nsGkAtoms::spelling, std::move(spellingErrors)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::spelling, DeleteEntry()); + } + } + + nsIFrame* frame = GetFrame(); + if (aCacheDomain & (CacheDomain::Text | CacheDomain::Bounds) && + !HasChildren()) { + // We cache line start offsets for both text and non-text leaf Accessibles + // because non-text leaf Accessibles can still start a line. + TextLeafPoint lineStart = + TextLeafPoint(this, 0).FindNextLineStartSameLocalAcc( + /* aIncludeOrigin */ true); + int32_t lineStartOffset = lineStart ? lineStart.mOffset : -1; + // We push line starts and text bounds in two cases: + // 1. Text or bounds changed, which means it's very likely that line starts + // and text bounds changed too. + // 2. CacheDomain::Bounds was requested (indicating that the frame was + // reflowed) but the bounds didn't actually change. This can happen when + // the spanned text is non-rectangular. For example, an Accessible might + // cover two characters on one line and a single character on another line. + // An insertion in a previous text node might cause it to shift such that it + // now covers a single character on the first line and two characters on the + // second line. Its bounding rect will be the same both before and after the + // insertion. In this case, we use the first line start to determine whether + // there was a change. This should be safe because the text didn't change in + // this Accessible, so if the first line start doesn't shift, none of them + // should shift. + if (aCacheDomain & CacheDomain::Text || boundsChanged || + mFirstLineStart != lineStartOffset) { + mFirstLineStart = lineStartOffset; + nsTArray<int32_t> lineStarts; + for (; lineStart; + lineStart = lineStart.FindNextLineStartSameLocalAcc(false)) { + lineStarts.AppendElement(lineStart.mOffset); + } + if (!lineStarts.IsEmpty()) { + fields->SetAttribute(nsGkAtoms::line, std::move(lineStarts)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::line, DeleteEntry()); + } + + if (frame && frame->IsTextFrame()) { + nsTArray<int32_t> charData; + + if (nsTextFrame* currTextFrame = do_QueryFrame(frame)) { + nsTextFrame* prevTextFrame = currTextFrame; + nsRect frameRect = currTextFrame->GetRect(); + nsIFrame* nearestAccAncestorFrame = + LocalParent() ? LocalParent()->GetFrame() : nullptr; + while (currTextFrame) { + nsRect contRect = currTextFrame->GetRect(); + if (prevTextFrame->GetParent() != currTextFrame->GetParent() && + nearestAccAncestorFrame) { + // Continuations can span multiple frame tree subtrees, + // particularly when multiline text is nested within both block + // and inline elements. In addition to using the position of this + // continuation to offset our char rects, we'll need to offset + // this continuation from the continuations that occurred before + // it. We don't know how many there are or what subtrees they're + // in, so we use a transform here. This also ensures our offset is + // accurate even if the intervening inline elements are not + // present in the a11y tree. + contRect = frameRect; + nsLayoutUtils::TransformRect(currTextFrame, + nearestAccAncestorFrame, contRect); + } + nsTArray<nsRect> charBounds; + currTextFrame->GetCharacterRectsInRange( + currTextFrame->GetContentOffset(), + currTextFrame->GetContentEnd(), charBounds); + for (const nsRect& charRect : charBounds) { + // We expect each char rect to be relative to the text leaf + // acc this text lives in. Unfortunately, GetCharacterRectsInRange + // returns rects relative to their continuation. Add the + // continuation's relative position here to make our final + // rect relative to the text leaf acc. Continuation rects include + // the padding of their parent text frame, so we compute the + // relative offset here instead of using `contRect`'s coordinates + // outright. + int computedX = charRect.x + (contRect.x - frameRect.x); + int computedY = charRect.y + (contRect.y - frameRect.y); + charData.AppendElement(computedX); + charData.AppendElement(computedY); + charData.AppendElement(charRect.width); + charData.AppendElement(charRect.height); + } + prevTextFrame = currTextFrame; + currTextFrame = currTextFrame->GetNextContinuation(); + } + } + if (charData.Length()) { + fields->SetAttribute(nsGkAtoms::characterData, std::move(charData)); + } + } + } + } + + if (aCacheDomain & CacheDomain::TransformMatrix) { + bool transformed = false; + if (frame && frame->IsTransformed()) { + // We need to find a frame to make our transform relative to. + // It's important this frame have a corresponding accessible, + // because this transform is applied while walking the accessibility + // tree (in the parent process), not the frame tree. + nsIFrame* boundingFrame = FindNearestAccessibleAncestorFrame(); + // This matrix is only valid when applied to CSSPixel points/rects + // in the coordinate space of `frame`. It also includes the translation + // to the parent space. + gfx::Matrix4x4Flagged mtx = nsLayoutUtils::GetTransformToAncestor( + RelativeTo{frame}, RelativeTo{boundingFrame}, nsIFrame::IN_CSS_UNITS); + // We might get back the identity matrix. This can happen if there is no + // actual transform. For example, if an element has + // will-change: transform, nsIFrame::IsTransformed will return true, but + // this doesn't necessarily mean there is a transform right now. + // Applying the identity matrix is effectively a no-op, so there's no + // point caching it. + transformed = !mtx.IsIdentity(); + if (transformed) { + UniquePtr<gfx::Matrix4x4> ptr = + MakeUnique<gfx::Matrix4x4>(mtx.GetMatrix()); + fields->SetAttribute(nsGkAtoms::transform, std::move(ptr)); + } + } + if (!transformed && aUpdateType == CacheUpdateType::Update) { + // Otherwise, if we're bundling a transform update but this + // frame isn't transformed (or doesn't exist), we need + // to send a DeleteEntry() to remove any + // transform that was previously cached for this frame. + fields->SetAttribute(nsGkAtoms::transform, DeleteEntry()); + } + } + + if (aCacheDomain & CacheDomain::ScrollPosition) { + nsPoint scrollPosition; + std::tie(scrollPosition, std::ignore) = mDoc->ComputeScrollData(this); + if (scrollPosition.x || scrollPosition.y) { + nsTArray<int32_t> positionArr(2); + positionArr.AppendElement(scrollPosition.x); + positionArr.AppendElement(scrollPosition.y); + fields->SetAttribute(nsGkAtoms::scrollPosition, std::move(positionArr)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::scrollPosition, DeleteEntry()); + } + } + + if (aCacheDomain & CacheDomain::DOMNodeID && mContent) { + nsAtom* id = mContent->GetID(); + if (id) { + fields->SetAttribute(nsGkAtoms::id, id); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::id, DeleteEntry()); + } + } + + // State is only included in the initial push. Thereafter, cached state is + // updated via events. + if (aCacheDomain & CacheDomain::State) { + if (aUpdateType == CacheUpdateType::Initial) { + // Most states are updated using state change events, so we only send + // these for the initial cache push. + uint64_t state = State(); + // Exclude states which must be calculated by RemoteAccessible. + state &= ~kRemoteCalculatedStates; + fields->SetAttribute(nsGkAtoms::state, state); + } + // If aria-selected isn't specified, there may be no SELECTED state. + // However, aria-selected can be implicit in some cases when an item is + // focused. We don't want to do this if aria-selected is explicitly + // set to "false", so we need to differentiate between false and unset. + if (auto ariaSelected = ARIASelected()) { + fields->SetAttribute(nsGkAtoms::aria_selected, *ariaSelected); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::aria_selected, DeleteEntry()); // Unset. + } + } + + if (aCacheDomain & CacheDomain::GroupInfo && mContent) { + for (nsAtom* attr : {nsGkAtoms::aria_level, nsGkAtoms::aria_setsize, + nsGkAtoms::aria_posinset}) { + int32_t value = 0; + if (nsCoreUtils::GetUIntAttr(mContent, attr, &value)) { + fields->SetAttribute(attr, value); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(attr, DeleteEntry()); + } + } + } + + if (aCacheDomain & CacheDomain::Actions) { + if (HasPrimaryAction()) { + // Here we cache the primary action. + nsAutoString actionName; + ActionNameAt(0, actionName); + RefPtr<nsAtom> actionAtom = NS_Atomize(actionName); + fields->SetAttribute(nsGkAtoms::action, actionAtom); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::action, DeleteEntry()); + } + + if (ImageAccessible* imgAcc = AsImage()) { + // Here we cache the showlongdesc action. + if (imgAcc->HasLongDesc()) { + fields->SetAttribute(nsGkAtoms::longdesc, true); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::longdesc, DeleteEntry()); + } + } + + KeyBinding accessKey = AccessKey(); + if (!accessKey.IsEmpty()) { + fields->SetAttribute(nsGkAtoms::accesskey, accessKey.Serialize()); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::accesskey, DeleteEntry()); + } + } + + if (aCacheDomain & CacheDomain::Style) { + if (RefPtr<nsAtom> display = DisplayStyle()) { + fields->SetAttribute(nsGkAtoms::display, display); + } + + float opacity = Opacity(); + if (opacity != 1.0f) { + fields->SetAttribute(nsGkAtoms::opacity, opacity); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::opacity, DeleteEntry()); + } + } + + if (aCacheDomain & CacheDomain::Table) { + if (TableAccessible* table = AsTable()) { + if (table->IsProbablyLayoutTable()) { + fields->SetAttribute(nsGkAtoms::layout_guess, true); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::layout_guess, DeleteEntry()); + } + } else if (TableCellAccessible* cell = AsTableCell()) { + // For HTML table cells, we must use the HTMLTableCellAccessible + // GetRow/ColExtent methods 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. + int32_t value = static_cast<int32_t>(cell->RowExtent()); + MOZ_ASSERT(value > 0); + if (value > 1) { + fields->SetAttribute(nsGkAtoms::rowspan, value); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::rowspan, DeleteEntry()); + } + value = static_cast<int32_t>(cell->ColExtent()); + MOZ_ASSERT(value > 0); + if (value > 1) { + fields->SetAttribute(nsGkAtoms::colspan, value); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::colspan, DeleteEntry()); + } + if (mContent->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::headers)) { + nsTArray<uint64_t> headers; + IDRefsIterator iter(mDoc, mContent, nsGkAtoms::headers); + while (LocalAccessible* cell = iter.Next()) { + if (cell->IsTableCell()) { + headers.AppendElement(cell->ID()); + } + } + fields->SetAttribute(nsGkAtoms::headers, std::move(headers)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::headers, DeleteEntry()); + } + } + } + + if (aCacheDomain & CacheDomain::ARIA && mContent && mContent->IsElement()) { + // We use a nested AccAttributes to make cache updates simpler. Rather than + // managing individual removals, we just replace or remove the entire set of + // ARIA attributes. + RefPtr<AccAttributes> ariaAttrs; + aria::AttrIterator attrIt(mContent); + while (attrIt.Next()) { + if (!ariaAttrs) { + ariaAttrs = new AccAttributes(); + } + attrIt.ExposeAttr(ariaAttrs); + } + if (ariaAttrs) { + fields->SetAttribute(nsGkAtoms::aria, std::move(ariaAttrs)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(nsGkAtoms::aria, DeleteEntry()); + } + } + + if (aCacheDomain & CacheDomain::Relations && mContent) { + if (IsHTMLRadioButton() || + (mContent->IsElement() && + mContent->AsElement()->IsHTMLElement(nsGkAtoms::a))) { + // HTML radio buttons with the same name should be grouped + // and returned together when their MEMBER_OF relation is + // requested. Computing LINKS_TO also requires we cache `name` on + // anchor elements. + nsString name; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); + if (!name.IsEmpty()) { + fields->SetAttribute(nsGkAtoms::attributeName, std::move(name)); + } else if (aUpdateType != CacheUpdateType::Initial) { + // It's possible we used to have a name and it's since been + // removed. Send a delete entry. + fields->SetAttribute(nsGkAtoms::attributeName, DeleteEntry()); + } + } + + for (auto const& data : kRelationTypeAtoms) { + nsTArray<uint64_t> ids; + nsStaticAtom* const relAtom = data.mAtom; + + Relation rel; + if (data.mType == RelationType::LABEL_FOR) { + // Labels are a special case -- we need to validate that the target of + // their `for` attribute is in fact labelable. DOM checks this when we + // call GetControl(). If a label contains an element we will return it + // here. + if (dom::HTMLLabelElement* labelEl = + dom::HTMLLabelElement::FromNode(mContent)) { + rel.AppendTarget(mDoc, labelEl->GetControl()); + } + } else { + // We use an IDRefsIterator here instead of calling RelationByType + // directly because we only want to cache explicit relations. Implicit + // relations will be computed and stored separately in the parent + // process. + rel.AppendIter(new IDRefsIterator(mDoc, mContent, relAtom)); + } + + while (LocalAccessible* acc = rel.LocalNext()) { + ids.AppendElement(acc->ID()); + } + if (ids.Length()) { + fields->SetAttribute(relAtom, std::move(ids)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(relAtom, DeleteEntry()); + } + } + } + +#if defined(XP_WIN) + if (aCacheDomain & CacheDomain::InnerHTML && HasOwnContent() && + mContent->IsMathMLElement(nsGkAtoms::math)) { + nsString innerHTML; + mContent->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors()); + fields->SetAttribute(nsGkAtoms::html, std::move(innerHTML)); + } +#endif // defined(XP_WIN) + + if (aUpdateType == CacheUpdateType::Initial) { + // Add fields which never change and thus only need to be included in the + // initial cache push. + if (mContent && mContent->IsElement()) { + fields->SetAttribute(nsGkAtoms::tag, mContent->NodeInfo()->NameAtom()); + + dom::Element* el = mContent->AsElement(); + if (IsTextField() || IsDateTimeField()) { + // Cache text input types. Accessible is recreated if this changes, + // so it is considered immutable. + if (const nsAttrValue* attr = el->GetParsedAttr(nsGkAtoms::type)) { + RefPtr<nsAtom> inputType = attr->GetAsAtom(); + if (inputType) { + fields->SetAttribute(nsGkAtoms::textInputType, inputType); + } + } + } + + // Changing the role attribute currently re-creates the Accessible, so + // it's immutable in the cache. + if (const nsRoleMapEntry* roleMap = ARIARoleMap()) { + // Most of the time, the role attribute is a single, known role. We + // already send the map index, so we don't need to double up. + if (!nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::role, roleMap->roleAtom, + eIgnoreCase)) { + // Multiple roles or unknown roles are rare, so just send them as a + // string. + nsAutoString role; + nsAccUtils::GetARIAAttr(el, nsGkAtoms::role, role); + fields->SetAttribute(nsGkAtoms::role, std::move(role)); + } + } + } + + if (frame) { + // Note our frame's current computed style so we can track style changes + // later on. + mOldComputedStyle = frame->Style(); + if (frame->IsTransformed()) { + mStateFlags |= eOldFrameHasValidTransformStyle; + } else { + mStateFlags &= ~eOldFrameHasValidTransformStyle; + } + } + + if (IsDoc()) { + if (PresShell* presShell = AsDoc()->PresShellPtr()) { + // Send the initial resolution of the document. When this changes, we + // will ne notified via nsAS::NotifyOfResolutionChange + float resolution = presShell->GetResolution(); + fields->SetAttribute(nsGkAtoms::resolution, resolution); + int32_t appUnitsPerDevPixel = + presShell->GetPresContext()->AppUnitsPerDevPixel(); + fields->SetAttribute(nsGkAtoms::_moz_device_pixel_ratio, + appUnitsPerDevPixel); + } + } + } + + if ((aCacheDomain & (CacheDomain::Text | CacheDomain::ScrollPosition) || + boundsChanged) && + mDoc) { + mDoc->SetViewportCacheDirty(true); + } + + return fields.forget(); +} + +void LocalAccessible::MaybeQueueCacheUpdateForStyleChanges() { + // mOldComputedStyle might be null if the initial cache hasn't been sent yet. + // In that case, there is nothing to do here. + if (!IPCAccessibilityActive() || + !StaticPrefs::accessibility_cache_enabled_AtStartup() || + !mOldComputedStyle) { + return; + } + + if (nsIFrame* frame = GetFrame()) { + const ComputedStyle* newStyle = frame->Style(); + + nsAutoCString oldDisplay, newDisplay; + mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_display, + oldDisplay); + newStyle->GetComputedPropertyValue(eCSSProperty_display, newDisplay); + + nsAutoCString oldOpacity, newOpacity; + mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_opacity, + oldOpacity); + newStyle->GetComputedPropertyValue(eCSSProperty_opacity, newOpacity); + + if (oldDisplay != newDisplay || oldOpacity != newOpacity) { + // CacheDomain::Style covers both display and opacity, so if + // either property has changed, send an update for the entire domain. + mDoc->QueueCacheUpdate(this, CacheDomain::Style); + } + + bool newHasValidTransformStyle = + newStyle->StyleDisplay()->HasTransform(frame); + bool oldHasValidTransformStyle = + (mStateFlags & eOldFrameHasValidTransformStyle) != 0; + + // We should send a transform update if we're adding or + // removing transform styling altogether. + bool sendTransformUpdate = + newHasValidTransformStyle || oldHasValidTransformStyle; + + if (newHasValidTransformStyle && oldHasValidTransformStyle) { + // If we continue to have transform styling, verify + // our transform has actually changed. + nsChangeHint transformHint = + newStyle->StyleDisplay()->CalcTransformPropertyDifference( + *mOldComputedStyle->StyleDisplay()); + // If this hint exists, it implies we found a property difference + sendTransformUpdate = !!transformHint; + } + + if (sendTransformUpdate) { + // If our transform matrix has changed, it's possible our + // viewport cache has also changed. + mDoc->SetViewportCacheDirty(true); + // Queuing a cache update for the TransformMatrix domain doesn't + // necessarily mean we'll send the matrix itself, we may + // send a DeleteEntry() instead. See BundleFieldsForCache for + // more information. + mDoc->QueueCacheUpdate(this, CacheDomain::TransformMatrix); + } + + if (newStyle->StyleDisplay()->IsPositionedStyle()) { + // We normally rely on reflow to know when bounds might have changed. + // However, changing the CSS left, top, etc. properties doesn't always + // cause reflow. + for (auto prop : {eCSSProperty_left, eCSSProperty_right, eCSSProperty_top, + eCSSProperty_bottom}) { + nsAutoCString oldVal, newVal; + mOldComputedStyle->GetComputedPropertyValue(prop, oldVal); + newStyle->GetComputedPropertyValue(prop, newVal); + if (oldVal != newVal) { + mDoc->QueueCacheUpdate(this, CacheDomain::Bounds); + break; + } + } + } + + mOldComputedStyle = newStyle; + if (newHasValidTransformStyle) { + mStateFlags |= eOldFrameHasValidTransformStyle; + } else { + mStateFlags &= ~eOldFrameHasValidTransformStyle; + } + } +} + +nsAtom* LocalAccessible::TagName() const { + return mContent && mContent->IsElement() ? mContent->NodeInfo()->NameAtom() + : nullptr; +} + +already_AddRefed<nsAtom> LocalAccessible::DisplayStyle() const { + if (dom::Element* elm = Elm()) { + if (elm->IsHTMLElement(nsGkAtoms::area)) { + // This is an image map area. CSS is irrelevant here. Furthermore, we + // won't be able to get the computed style if the map is unslotted in a + // shadow host. + return nullptr; + } + StyleInfo info(elm); + return info.Display(); + } + return nullptr; +} + +float LocalAccessible::Opacity() const { + if (nsIFrame* frame = GetFrame()) { + return frame->StyleEffects()->mOpacity; + } + + return 1.0f; +} + +void LocalAccessible::DOMNodeID(nsString& aID) const { + aID.Truncate(); + if (mContent) { + if (nsAtom* id = mContent->GetID()) { + id->ToString(aID); + } + } +} + +void LocalAccessible::LiveRegionAttributes(nsAString* aLive, + nsAString* aRelevant, + Maybe<bool>* aAtomic, + nsAString* aBusy) const { + dom::Element* el = Elm(); + if (!el) { + return; + } + if (aLive) { + nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_live, *aLive); + } + if (aRelevant) { + nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_relevant, *aRelevant); + } + if (aAtomic) { + // XXX We ignore aria-atomic="false", but this probably doesn't conform to + // the spec. + if (nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::aria_atomic, + nsGkAtoms::_true, eCaseMatters)) { + *aAtomic = Some(true); + } + } + if (aBusy) { + nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_busy, *aBusy); + } +} + +Maybe<bool> LocalAccessible::ARIASelected() const { + if (dom::Element* el = Elm()) { + nsStaticAtom* atom = + nsAccUtils::NormalizeARIAToken(el, nsGkAtoms::aria_selected); + if (atom == nsGkAtoms::_true) { + return Some(true); + } + if (atom == nsGkAtoms::_false) { + return Some(false); + } + } + return Nothing(); +} + +void LocalAccessible::StaticAsserts() const { + static_assert( + eLastStateFlag <= (1 << kStateFlagsBits) - 1, + "LocalAccessible::mStateFlags was oversized by eLastStateFlag!"); + static_assert( + eLastContextFlag <= (1 << kContextFlagsBits) - 1, + "LocalAccessible::mContextFlags was oversized by eLastContextFlag!"); +} + +TableAccessibleBase* LocalAccessible::AsTableBase() { + if (StaticPrefs::accessibility_cache_enabled_AtStartup() && IsTable() && + !mContent->IsXULElement()) { + // This isn't strictly related to caching, but this new table implementation + // is being developed to make caching feasible. We put it behind this pref + // to make it easy to test while it's still under development. + return CachedTableAccessible::GetFrom(this); + } + return AsTable(); +} + +TableCellAccessibleBase* LocalAccessible::AsTableCellBase() { + if (StaticPrefs::accessibility_cache_enabled_AtStartup() && IsTableCell() && + !mContent->IsXULElement()) { + // This isn't strictly related to caching, but this new table implementation + // is being developed to make caching feasible. We put it behind this pref + // to make it easy to test while it's still under development. + return CachedTableCellAccessible::GetFrom(this); + } + return AsTableCell(); +} + +Maybe<int32_t> LocalAccessible::GetIntARIAAttr(nsAtom* aAttrName) const { + if (mContent) { + int32_t val; + if (nsCoreUtils::GetUIntAttr(mContent, aAttrName, &val)) { + return Some(val); + } + // XXX Handle attributes that allow -1; e.g. aria-row/colcount. + } + return Nothing(); +} diff --git a/accessible/generic/LocalAccessible.h b/accessible/generic/LocalAccessible.h new file mode 100644 index 0000000000..173cb41cab --- /dev/null +++ b/accessible/generic/LocalAccessible.h @@ -0,0 +1,1066 @@ +/* -*- 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 _LocalAccessible_H_ +#define _LocalAccessible_H_ + +#include "mozilla/ComputedStyle.h" +#include "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/AccTypes.h" +#include "mozilla/a11y/RelationType.h" +#include "mozilla/a11y/States.h" + +#include "mozilla/UniquePtr.h" + +#include "nsIContent.h" +#include "nsTArray.h" +#include "nsRefPtrHashtable.h" +#include "nsRect.h" + +struct nsRoleMapEntry; + +class nsIFrame; + +class nsAttrValue; + +namespace mozilla::dom { +class Element; +} + +namespace mozilla { +namespace a11y { + +class LocalAccessible; +class AccAttributes; +class AccEvent; +class AccGroupInfo; +class ApplicationAccessible; +class CacheData; +class DocAccessible; +class EmbeddedObjCollector; +class EventTree; +class HTMLImageMapAccessible; +class HTMLLIAccessible; +class HTMLLinkAccessible; +class HyperTextAccessible; +class HyperTextAccessibleBase; +class ImageAccessible; +class KeyBinding; +class OuterDocAccessible; +class RemoteAccessible; +class Relation; +class RootAccessible; +class TableAccessible; +class TableAccessibleBase; +class TableCellAccessible; +class TableCellAccessibleBase; +class TextLeafAccessible; +class XULLabelAccessible; +class XULTreeAccessible; + +enum class CacheUpdateType; + +#ifdef A11Y_LOG +namespace logging { +typedef const char* (*GetTreePrefix)(void* aData, LocalAccessible*); +void Tree(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot, + GetTreePrefix aPrefixFunc, void* GetTreePrefixData); +void TreeSize(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot); +}; // namespace logging +#endif + +typedef nsRefPtrHashtable<nsPtrHashKey<const void>, LocalAccessible> + AccessibleHashtable; + +#define NS_ACCESSIBLE_IMPL_IID \ + { /* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \ + 0x133c8bf4, 0x4913, 0x4355, { \ + 0xbd, 0x50, 0x42, 0x6b, 0xd1, 0xd6, 0xe1, 0xad \ + } \ + } + +/** + * An accessibility tree node that originated in mDoc's content process. + */ +class LocalAccessible : public nsISupports, public Accessible { + public: + LocalAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(LocalAccessible) + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_ACCESSIBLE_IMPL_IID) + + ////////////////////////////////////////////////////////////////////////////// + // Public methods + + /** + * Return the document accessible for this accessible. + */ + DocAccessible* Document() const { return mDoc; } + + /** + * Return the root document accessible for this accessible. + */ + a11y::RootAccessible* RootAccessible() const; + + /** + * Return frame for this accessible. + * Note that this will return null for display: contents. Also, + * DocAccessible::GetFrame can return null if the frame tree hasn't been + * created yet. + */ + virtual nsIFrame* GetFrame() const; + + /** + * Return DOM node associated with the accessible. + */ + virtual nsINode* GetNode() const; + + nsIContent* GetContent() const { return mContent; } + dom::Element* Elm() const; + + /** + * Return node type information of DOM node associated with the accessible. + */ + bool IsContent() const { return GetNode() && GetNode()->IsContent(); } + + /** + * Return the unique identifier of the accessible. + * ID() should be preferred, but this method still exists because many + * LocalAccessible callers expect a void*. + */ + void* UniqueID() { return static_cast<void*>(this); } + + virtual uint64_t ID() const override { + return IsDoc() ? 0 : reinterpret_cast<uintptr_t>(this); + } + + /** + * Return language associated with the accessible. + */ + void Language(nsAString& aLocale); + + /** + * Get the description of this accessible. + */ + virtual void Description(nsString& aDescription) const override; + + /** + * Get the value of this accessible. + */ + virtual void Value(nsString& aValue) const override; + + /** + * Get help string for the accessible. + */ + void Help(nsString& aHelp) const { aHelp.Truncate(); } + + /** + * Get the name of this accessible. + */ + virtual ENameValueFlag Name(nsString& aName) const override; + + /** + * Maps ARIA state attributes to state of accessible. Note the given state + * argument should hold states for accessible before you pass it into this + * method. + * + * @param [in/out] where to fill the states into. + */ + virtual void ApplyARIAState(uint64_t* aState) const; + + /** + * Return enumerated accessible role (see constants in Role.h). + */ + virtual mozilla::a11y::role Role() const override; + + /** + * Return accessible role specified by ARIA (see constants in + * roles). + */ + mozilla::a11y::role ARIARole(); + + /** + * Returns enumerated accessible role from native markup (see constants in + * Role.h). Doesn't take into account ARIA roles. + */ + virtual mozilla::a11y::role NativeRole() const; + + virtual uint64_t State() override; + + /** + * Return interactive states present on the accessible + * (@see NativeInteractiveState). + */ + uint64_t InteractiveState() const { + uint64_t state = NativeInteractiveState(); + ApplyARIAState(&state); + return state; + } + + /** + * Return link states present on the accessible. + */ + uint64_t LinkState() const { + uint64_t state = NativeLinkState(); + ApplyARIAState(&state); + return state; + } + + /** + * Return the states of accessible, not taking into account ARIA states. + * Use State() to get complete set of states. + */ + virtual uint64_t NativeState() const; + + /** + * Return native interactice state (unavailable, focusable or selectable). + */ + virtual uint64_t NativeInteractiveState() const; + + /** + * Return native link states present on the accessible. + */ + virtual uint64_t NativeLinkState() const; + + /** + * Return bit set of invisible and offscreen states. + */ + uint64_t VisibilityState() const; + + /** + * Return true if native unavailable state present. + */ + virtual bool NativelyUnavailable() const; + + virtual already_AddRefed<AccAttributes> Attributes() override; + + /** + * Return direct or deepest child at the given point. + * + * @param aX [in] x coordinate relative screen + * @param aY [in] y coordinate relative screen + * @param aWhichChild [in] flag points if deepest or direct child + * should be returned + */ + virtual LocalAccessible* LocalChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild); + + /** + * Similar to LocalChildAtPoint but crosses process boundaries. + */ + virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) override; + + virtual Relation RelationByType(RelationType aType) const override; + + ////////////////////////////////////////////////////////////////////////////// + // Initializing methods + + /** + * Shutdown this accessible object. + */ + virtual void Shutdown(); + + /** + * Set the ARIA role map entry for a new accessible. + */ + void SetRoleMapEntry(const nsRoleMapEntry* aRoleMapEntry); + + /** + * Append/insert/remove a child. Return true if operation was successful. + */ + bool AppendChild(LocalAccessible* aChild) { + return InsertChildAt(mChildren.Length(), aChild); + } + virtual bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild); + + /** + * Inserts a child after given sibling. If the child cannot be inserted, + * then the child is unbound from the document, and false is returned. Make + * sure to null out any references on the child object as it may be destroyed. + */ + bool InsertAfter(LocalAccessible* aNewChild, LocalAccessible* aRefChild); + + virtual bool RemoveChild(LocalAccessible* aChild); + + /** + * Reallocates the child within its parent. + */ + virtual void RelocateChild(uint32_t aNewIndex, LocalAccessible* aChild); + + // Accessible hierarchy method overrides + + virtual Accessible* Parent() const override { return LocalParent(); } + + virtual Accessible* ChildAt(uint32_t aIndex) const override { + return LocalChildAt(aIndex); + } + + virtual Accessible* NextSibling() const override { + return LocalNextSibling(); + } + + virtual Accessible* PrevSibling() const override { + return LocalPrevSibling(); + } + + ////////////////////////////////////////////////////////////////////////////// + // LocalAccessible tree traverse methods + + /** + * Return parent accessible. + */ + LocalAccessible* LocalParent() const { return mParent; } + + /** + * Return child accessible at the given index. + */ + virtual LocalAccessible* LocalChildAt(uint32_t aIndex) const; + + /** + * Return child accessible count. + */ + virtual uint32_t ChildCount() const override; + + /** + * Return index of the given child accessible. + */ + int32_t GetIndexOf(const LocalAccessible* aChild) const { + return (aChild->mParent != this) ? -1 : aChild->IndexInParent(); + } + + /** + * Return index in parent accessible. + */ + virtual int32_t IndexInParent() const override; + + /** + * Return first/last/next/previous sibling of the accessible. + */ + inline LocalAccessible* LocalNextSibling() const { + return GetSiblingAtOffset(1); + } + inline LocalAccessible* LocalPrevSibling() const { + return GetSiblingAtOffset(-1); + } + inline LocalAccessible* LocalFirstChild() const { return LocalChildAt(0); } + inline LocalAccessible* LocalLastChild() const { + uint32_t childCount = ChildCount(); + return childCount != 0 ? LocalChildAt(childCount - 1) : nullptr; + } + + virtual uint32_t EmbeddedChildCount() override; + + /** + * Return embedded accessible child at the given index. + */ + virtual LocalAccessible* EmbeddedChildAt(uint32_t aIndex) override; + + virtual int32_t IndexOfEmbeddedChild(Accessible* aChild) override; + + /** + * Return number of content children/content child at index. The content + * child is created from markup in contrast to it's never constructed by its + * parent accessible (like treeitem accessibles for XUL trees). + */ + uint32_t ContentChildCount() const { return mChildren.Length(); } + LocalAccessible* ContentChildAt(uint32_t aIndex) const { + return mChildren.ElementAt(aIndex); + } + + /** + * Return true if the accessible is attached to tree. + */ + bool IsBoundToParent() const { return !!mParent; } + + ////////////////////////////////////////////////////////////////////////////// + // Miscellaneous methods + + /** + * Handle accessible event, i.e. process it, notifies observers and fires + * platform specific event. + */ + virtual nsresult HandleAccEvent(AccEvent* aAccEvent); + + /** + * Return true if the accessible is an acceptable child. + */ + virtual bool IsAcceptableChild(nsIContent* aEl) const { + return aEl && + !aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup); + } + + virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0, + uint32_t aLength = UINT32_MAX) override; + + virtual nsRect BoundsInAppUnits() const override; + + virtual LayoutDeviceIntRect Bounds() const override; + + /** + * Return boundaries rect relative the bounding frame. + */ + virtual nsRect RelativeBounds(nsIFrame** aRelativeFrame) const; + + /** + * Return boundaries rect relative to the frame of the parent accessible. + * The returned bounds are the same regardless of whether the parent is + * scrolled. This means the scroll position must be later subtracted to + * calculate absolute coordinates. + */ + virtual nsRect ParentRelativeBounds(); + + /** + * Selects the accessible within its container if applicable. + */ + virtual void SetSelected(bool aSelect) override; + + /** + * Select the accessible within its container. + */ + virtual void TakeSelection() override; + + /** + * Focus the accessible. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void TakeFocus() const override; + + MOZ_CAN_RUN_SCRIPT + virtual void ScrollTo(uint32_t aHow) const override; + + /** + * Scroll the accessible to the given point. + */ + void ScrollToPoint(uint32_t aCoordinateType, int32_t aX, int32_t aY); + + /** + * Get a pointer to accessibility interface for this node, which is specific + * to the OS/accessibility toolkit we're running on. + */ + virtual void GetNativeInterface(void** aNativeAccessible); + + virtual Maybe<int32_t> GetIntARIAAttr(nsAtom* aAttrName) const override; + + ////////////////////////////////////////////////////////////////////////////// + // Downcasting and types + + inline bool IsAbbreviation() const { + return mContent && + mContent->IsAnyOfHTMLElements(nsGkAtoms::abbr, nsGkAtoms::acronym); + } + + ApplicationAccessible* AsApplication(); + + DocAccessible* AsDoc(); + + HyperTextAccessible* AsHyperText(); + virtual HyperTextAccessibleBase* AsHyperTextBase() override; + + HTMLLIAccessible* AsHTMLListItem(); + + HTMLLinkAccessible* AsHTMLLink(); + + ImageAccessible* AsImage(); + + HTMLImageMapAccessible* AsImageMap(); + + OuterDocAccessible* AsOuterDoc(); + + a11y::RootAccessible* AsRoot(); + + bool IsSearchbox() const; + + virtual TableAccessible* AsTable() { return nullptr; } + + virtual TableCellAccessible* AsTableCell() { return nullptr; } + const TableCellAccessible* AsTableCell() const { + return const_cast<LocalAccessible*>(this)->AsTableCell(); + } + + virtual TableAccessibleBase* AsTableBase() override; + virtual TableCellAccessibleBase* AsTableCellBase() override; + + TextLeafAccessible* AsTextLeaf(); + + XULLabelAccessible* AsXULLabel(); + + XULTreeAccessible* AsXULTree(); + + ////////////////////////////////////////////////////////////////////////////// + // ActionAccessible + + virtual bool HasPrimaryAction() const override; + + virtual uint8_t ActionCount() const override; + + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + + virtual bool DoAction(uint8_t aIndex) const override; + + virtual KeyBinding AccessKey() const override; + + /** + * Return global keyboard shortcut for default action, such as Ctrl+O for + * Open file menuitem. + */ + virtual KeyBinding KeyboardShortcut() const; + + ////////////////////////////////////////////////////////////////////////////// + // HyperLinkAccessible (any embedded object in text can implement HyperLink, + // which helps determine where it is located within containing text). + + /** + * Return true if the accessible is hyper link accessible. + */ + virtual bool IsLink() const override; + + /** + * Return true if the link is valid (e. g. points to a valid URL). + */ + inline bool IsLinkValid() { + MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!"); + + // XXX In order to implement this we would need to follow every link + // Perhaps we can get information about invalid links from the cache + // In the mean time authors can use role="link" aria-invalid="true" + // to force it for links they internally know to be invalid + return (0 == (State() & mozilla::a11y::states::INVALID)); + } + + /** + * Return the number of anchors within the link. + */ + virtual uint32_t AnchorCount(); + + /** + * Returns an anchor accessible at the given index. + */ + virtual LocalAccessible* AnchorAt(uint32_t aAnchorIndex); + + /** + * Returns an anchor URI at the given index. + */ + virtual already_AddRefed<nsIURI> AnchorURIAt(uint32_t aAnchorIndex) const; + + ////////////////////////////////////////////////////////////////////////////// + // SelectAccessible + + /** + * Return an array of selected items. + */ + virtual void SelectedItems(nsTArray<Accessible*>* aItems) override; + + /** + * Return the number of selected items. + */ + virtual uint32_t SelectedItemCount() override; + + /** + * Return selected item at the given index. + */ + virtual Accessible* GetSelectedItem(uint32_t aIndex) override; + + /** + * Determine if item at the given index is selected. + */ + virtual bool IsItemSelected(uint32_t aIndex) override; + + /** + * Add item at the given index the selection. Return true if success. + */ + virtual bool AddItemToSelection(uint32_t aIndex) override; + + /** + * Remove item at the given index from the selection. Return if success. + */ + virtual bool RemoveItemFromSelection(uint32_t aIndex) override; + + /** + * Select all items. Return true if success. + */ + virtual bool SelectAll() override; + + /** + * Unselect all items. Return true if success. + */ + virtual bool UnselectAll() override; + + ////////////////////////////////////////////////////////////////////////////// + // Value (numeric value interface) + + virtual double MaxValue() const override; + virtual double MinValue() const override; + virtual double CurValue() const override; + virtual double Step() const override; + virtual bool SetCurValue(double aValue); + + ////////////////////////////////////////////////////////////////////////////// + // Widgets + + /** + * Return true if accessible is a widget, i.e. control or accessible that + * manages its items. Note, being a widget the accessible may be a part of + * composite widget. + */ + virtual bool IsWidget() const; + + /** + * Return true if the widget is active, i.e. has a focus within it. + */ + virtual bool IsActiveWidget() const; + + /** + * Return true if the widget has items and items are operable by user and + * can be activated. + */ + virtual bool AreItemsOperable() const; + + /** + * Return the current item of the widget, i.e. an item that has or will have + * keyboard focus when widget gets active. + */ + virtual LocalAccessible* CurrentItem() const; + + /** + * Set the current item of the widget. + */ + virtual void SetCurrentItem(const LocalAccessible* aItem); + + /** + * Return container widget this accessible belongs to. + */ + virtual LocalAccessible* ContainerWidget() const; + + bool IsActiveDescendant(LocalAccessible** aWidget = nullptr) const; + + /** + * Return true if the accessible is defunct. + */ + bool IsDefunct() const; + + /** + * Return false if the accessible is no longer in the document. + */ + bool IsInDocument() const { return !(mStateFlags & eIsNotInDocument); } + + /** + * Return true if the accessible should be contained by document node map. + */ + bool IsNodeMapEntry() const { + return HasOwnContent() && !(mStateFlags & eNotNodeMapEntry); + } + + /** + * Return true if the accessible has associated DOM content. + */ + bool HasOwnContent() const { + return mContent && !(mStateFlags & eSharedNode); + } + + /** + * Return true if native markup has a numeric value. + */ + bool NativeHasNumericValue() const; + + /** + * Return true if ARIA specifies support for a numeric value. + */ + bool ARIAHasNumericValue() const; + + /** + * Return true if the accessible has a numeric value. + */ + virtual bool HasNumericValue() const override; + + /** + * Return true if the accessible state change is processed by handling proper + * DOM UI event, if otherwise then false. For example, CheckboxAccessible + * created for HTML:input@type="checkbox" will process + * nsIDocumentObserver::ElementStateChanged instead of 'CheckboxStateChange' + * event. + */ + bool NeedsDOMUIEvent() const { return !(mStateFlags & eIgnoreDOMUIEvent); } + + /** + * Get/set repositioned bit indicating that the accessible was moved in + * the accessible tree, i.e. the accessible tree structure differs from DOM. + */ + bool IsRelocated() const { return mStateFlags & eRelocated; } + void SetRelocated(bool aRelocated) { + if (aRelocated) { + mStateFlags |= eRelocated; + } else { + mStateFlags &= ~eRelocated; + } + } + + /** + * Return true if the accessible allows accessible children from subtree of + * a DOM element of this accessible. + */ + bool KidsFromDOM() const { return !(mStateFlags & eNoKidsFromDOM); } + + /** + * Return true if this accessible has a parent, relation or ancestor with a + * relation whose name depends on this accessible. + */ + bool HasNameDependent() const { return mContextFlags & eHasNameDependent; } + + /** + * Return true if this accessible has a parent, relation or ancestor with a + * relation whose description depends on this accessible. + */ + bool HasDescriptionDependent() const { + return mContextFlags & eHasDescriptionDependent; + } + + /** + * Return true if the element is inside an alert. + */ + bool IsInsideAlert() const { return mContextFlags & eInsideAlert; } + + /** + * Return true if there is a pending reorder event for this accessible. + */ + bool ReorderEventTarget() const { return mReorderEventTarget; } + + /** + * Return true if there is a pending show event for this accessible. + */ + bool ShowEventTarget() const { return mShowEventTarget; } + + /** + * Return true if there is a pending hide event for this accessible. + */ + bool HideEventTarget() const { return mHideEventTarget; } + + /** + * Set if there is a pending reorder event for this accessible. + */ + void SetReorderEventTarget(bool aTarget) { mReorderEventTarget = aTarget; } + + /** + * Set if this accessible is a show event target. + */ + void SetShowEventTarget(bool aTarget) { mShowEventTarget = aTarget; } + + /** + * Set if this accessible is a hide event target. + */ + void SetHideEventTarget(bool aTarget) { mHideEventTarget = aTarget; } + + void Announce(const nsAString& aAnnouncement, uint16_t aPriority); + + virtual bool IsRemote() const override { return false; } + + already_AddRefed<AccAttributes> BundleFieldsForCache( + uint64_t aCacheDomain, CacheUpdateType aUpdateType); + + /** + * Push fields to cache. + * aCacheDomain - describes which fields to bundle and ultimately send + * aUpdate - describes whether this is an initial or subsequent update + */ + void SendCache(uint64_t aCacheDomain, CacheUpdateType aUpdate); + + void MaybeQueueCacheUpdateForStyleChanges(); + + virtual nsAtom* TagName() const override; + + virtual already_AddRefed<nsAtom> DisplayStyle() const override; + + virtual float Opacity() const override; + + virtual void DOMNodeID(nsString& aID) const override; + + virtual void LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant, + Maybe<bool>* aAtomic, + nsAString* aBusy) const override; + + virtual Maybe<bool> ARIASelected() const override; + + protected: + virtual ~LocalAccessible(); + + /** + * Return the accessible name provided by native markup. It doesn't take + * into account ARIA markup used to specify the name. + */ + virtual mozilla::a11y::ENameValueFlag NativeName(nsString& aName) const; + + /** + * Return the accessible description provided by native markup. It doesn't + * take into account ARIA markup used to specify the description. + */ + void NativeDescription(nsString& aDescription) const; + + /** + * Return object attributes provided by native markup. It doesn't take into + * account ARIA. + */ + virtual already_AddRefed<AccAttributes> NativeAttributes(); + + /** + * The given attribute has the potential of changing the accessible's state. + * This is used to capture the state before the attribute change and compare + * it with the state after. + */ + virtual bool AttributeChangesState(nsAtom* aAttribute); + + /** + * Notify accessible that a DOM attribute on its associated content has + * changed. This allows the accessible to update its state and emit any + * relevant events. + */ + virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue, + uint64_t aOldState); + + ////////////////////////////////////////////////////////////////////////////// + // Initializing, cache and tree traverse methods + + /** + * Destroy the object. + */ + void LastRelease(); + + /** + * Set accessible parent and index in parent. + */ + void BindToParent(LocalAccessible* aParent, uint32_t aIndexInParent); + void UnbindFromParent(); + + /** + * Return sibling accessible at the given offset. + */ + virtual LocalAccessible* GetSiblingAtOffset(int32_t aOffset, + nsresult* aError = nullptr) const; + + void ModifySubtreeContextFlags(uint32_t aContextFlags, bool aAdd); + + /** + * Flags used to describe the state of this accessible. + */ + enum StateFlags { + eIsDefunct = 1 << 0, // accessible is defunct + eIsNotInDocument = 1 << 1, // accessible is not in document + eSharedNode = 1 << 2, // accessible shares DOM node from another accessible + eNotNodeMapEntry = 1 << 3, // accessible shouldn't be in document node map + eGroupInfoDirty = 1 << 4, // accessible needs to update group info + eKidsMutating = 1 << 5, // subtree is being mutated + eIgnoreDOMUIEvent = 1 << 6, // don't process DOM UI events for a11y events + eRelocated = 1 << 7, // accessible was moved in tree + eNoKidsFromDOM = 1 << 8, // accessible doesn't allow children from DOM + eHasTextKids = 1 << 9, // accessible have a text leaf in children + eOldFrameHasValidTransformStyle = + 1 << 10, // frame prior to most recent style change both has transform + // styling and supports transforms + + eLastStateFlag = eOldFrameHasValidTransformStyle + }; + + /** + * Flags used for contextual information about the accessible. + */ + enum ContextFlags { + eHasNameDependent = 1 << 0, // See HasNameDependent(). + eInsideAlert = 1 << 1, + eHasDescriptionDependent = 1 << 2, // See HasDescriptionDependent(). + + eLastContextFlag = eHasDescriptionDependent + }; + + protected: + ////////////////////////////////////////////////////////////////////////////// + // Miscellaneous helpers + + /** + * Return ARIA role (helper method). + */ + mozilla::a11y::role ARIATransformRole(mozilla::a11y::role aRole) const; + + ////////////////////////////////////////////////////////////////////////////// + // Name helpers + + /** + * Returns the accessible name specified by ARIA. + */ + void ARIAName(nsString& aName) const; + + /** + * Returns the accessible description specified by ARIA. + */ + void ARIADescription(nsString& aDescription) const; + + /** + * Returns the accessible name specified for this control using XUL + * <label control="id" ...>. + */ + static void NameFromAssociatedXULLabel(DocAccessible* aDocument, + nsIContent* aElm, nsString& aName); + + /** + * Return the name for XUL element. + */ + static void XULElmName(DocAccessible* aDocument, nsIContent* aElm, + nsString& aName); + + // helper method to verify frames + static nsresult GetFullKeyName(const nsAString& aModifierName, + const nsAString& aKeyName, + nsAString& aStringOut); + + ////////////////////////////////////////////////////////////////////////////// + // Action helpers + + /** + * Prepares click action that will be invoked in timeout. + * + * @note DoCommand() prepares an action in timeout because when action + * command opens a modal dialog/window, it won't return until the + * dialog/window is closed. If executing action command directly in + * nsIAccessible::DoAction() method, it will block AT tools (e.g. GOK) that + * invoke action of mozilla accessibles direclty (see bug 277888 for + * details). + * + * @param aContent [in, optional] element to click + * @param aActionIndex [in, optional] index of accessible action + */ + void DoCommand(nsIContent* aContent = nullptr, + uint32_t aActionIndex = 0) const; + + /** + * Dispatch click event. + */ + MOZ_CAN_RUN_SCRIPT + virtual void DispatchClickEvent(nsIContent* aContent, + uint32_t aActionIndex) const; + + ////////////////////////////////////////////////////////////////////////////// + // Helpers + + /** + * Get the container node for an atomic region, defined by aria-atomic="true" + * @return the container node + */ + nsIContent* GetAtomicRegion() const; + + /** + * Return numeric value of the given ARIA attribute, NaN if not applicable. + * + * @param aARIAProperty [in] the ARIA property we're using + * @return a numeric value + */ + double AttrNumericValue(nsAtom* aARIAAttr) const; + + /** + * Return the action rule based on ARIA enum constants EActionRule + * (see ARIAMap.h). Used by ActionCount() and ActionNameAt(). + */ + uint32_t GetActionRule() const; + + virtual AccGroupInfo* GetGroupInfo() const override; + + virtual AccGroupInfo* GetOrCreateGroupInfo() override; + + virtual void ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize, + int32_t* aPosInSet) const override; + + // Data Members + // mContent can be null in a DocAccessible if the document has no body or + // root element. + nsCOMPtr<nsIContent> mContent; + RefPtr<DocAccessible> mDoc; + + LocalAccessible* mParent; + nsTArray<LocalAccessible*> mChildren; + int32_t mIndexInParent; + + // These are used to determine whether to send cache updates. + Maybe<nsRect> mBounds; + int32_t mFirstLineStart; + + /** + * Maintain a reference to the ComputedStyle of our frame so we can + * send cache updates when style changes are observed. + * + * This RefPtr is initialised in BundleFieldsForCache to the ComputedStyle + * for our initial frame. + * Style changes are observed in one of two ways: + * 1. Style changes on the same frame are observed in + * nsIFrame::DidSetComputedStyle. + * 2. Style changes for reconstructed frames are handled in + * DocAccessible::PruneOrInsertSubtree. + * In both cases, we call into MaybeQueueCacheUpdateForStyleChanges. There, we + * compare a11y-relevant properties in mOldComputedStyle with the current + * ComputedStyle fetched from GetFrame()->Style(). Finally, we send cache + * updates for attributes affected by the style change and update + * mOldComputedStyle to the style of our current frame. + */ + RefPtr<const ComputedStyle> mOldComputedStyle; + + static const uint8_t kStateFlagsBits = 11; + static const uint8_t kContextFlagsBits = 3; + + /** + * Keep in sync with StateFlags, ContextFlags, and AccTypes. + */ + mutable uint32_t mStateFlags : kStateFlagsBits; + uint32_t mContextFlags : kContextFlagsBits; + uint32_t mReorderEventTarget : 1; + uint32_t mShowEventTarget : 1; + uint32_t mHideEventTarget : 1; + + void StaticAsserts() const; + +#ifdef A11Y_LOG + friend void logging::Tree(const char* aTitle, const char* aMsgText, + LocalAccessible* aRoot, + logging::GetTreePrefix aPrefixFunc, + void* aGetTreePrefixData); + friend void logging::TreeSize(const char* aTitle, const char* aMsgText, + LocalAccessible* aRoot); +#endif + friend class DocAccessible; + friend class xpcAccessible; + friend class TreeMutation; + + UniquePtr<mozilla::a11y::EmbeddedObjCollector> mEmbeddedObjCollector; + int32_t mIndexOfEmbeddedChild; + + friend class EmbeddedObjCollector; + + mutable AccGroupInfo* mGroupInfo; + friend class AccGroupInfo; + + private: + LocalAccessible() = delete; + LocalAccessible(const LocalAccessible&) = delete; + LocalAccessible& operator=(const LocalAccessible&) = delete; + + /** + * Traverses the accessible's parent chain in search of an accessible with + * a frame. Returns the frame when found. Includes special handling for + * OOP iframe docs and tab documents. + */ + nsIFrame* FindNearestAccessibleAncestorFrame(); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(LocalAccessible, NS_ACCESSIBLE_IMPL_IID) + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible downcasting method + +inline LocalAccessible* Accessible::AsLocal() { + return IsLocal() ? static_cast<LocalAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/OuterDocAccessible.cpp b/accessible/generic/OuterDocAccessible.cpp new file mode 100644 index 0000000000..3c03551e98 --- /dev/null +++ b/accessible/generic/OuterDocAccessible.cpp @@ -0,0 +1,236 @@ +/* -*- 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 "OuterDocAccessible.h" + +#include "LocalAccessible-inl.h" +#include "nsAccUtils.h" +#include "DocAccessible-inl.h" +#include "mozilla/a11y/DocAccessibleChild.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/dom/BrowserBridgeChild.h" +#include "mozilla/dom/BrowserParent.h" +#include "Role.h" +#include "States.h" + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// OuterDocAccessible +//////////////////////////////////////////////////////////////////////////////// + +OuterDocAccessible::OuterDocAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) { + mType = eOuterDocType; + +#ifdef XP_WIN + if (DocAccessibleParent* remoteDoc = RemoteChildDoc()) { + remoteDoc->SendParentCOMProxy(this); + } +#endif + + if (IPCAccessibilityActive()) { + auto bridge = dom::BrowserBridgeChild::GetFrom(aContent); + if (bridge) { + // This is an iframe which will be rendered in another process. + SendEmbedderAccessible(bridge); + } + } + + // Request document accessible for the content document to make sure it's + // created. It will appended to outerdoc accessible children asynchronously. + dom::Document* outerDoc = mContent->GetUncomposedDoc(); + if (outerDoc) { + dom::Document* innerDoc = outerDoc->GetSubDocumentFor(mContent); + if (innerDoc) GetAccService()->GetDocAccessible(innerDoc); + } +} + +OuterDocAccessible::~OuterDocAccessible() {} + +void OuterDocAccessible::SendEmbedderAccessible( + dom::BrowserBridgeChild* aBridge) { + MOZ_ASSERT(mDoc); + DocAccessibleChild* ipcDoc = mDoc->IPCDoc(); + if (ipcDoc) { + uint64_t id = reinterpret_cast<uintptr_t>(UniqueID()); +#if defined(XP_WIN) + ipcDoc->SetEmbedderOnBridge(aBridge, id); +#else + aBridge->SetEmbedderAccessible(ipcDoc, id); +#endif + } +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible public (DON'T add methods here) + +role OuterDocAccessible::NativeRole() const { return roles::INTERNAL_FRAME; } + +LocalAccessible* OuterDocAccessible::LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) { + LayoutDeviceIntRect docRect = Bounds(); + if (!docRect.Contains(aX, aY)) return nullptr; + + // Always return the inner doc as direct child accessible unless bounds + // outside of it. + LocalAccessible* child = LocalChildAt(0); + NS_ENSURE_TRUE(child, nullptr); + + if (aWhichChild == Accessible::EWhichChildAtPoint::DeepestChild) { + return child->LocalChildAtPoint( + aX, aY, Accessible::EWhichChildAtPoint::DeepestChild); + } + return child; +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible public + +void OuterDocAccessible::Shutdown() { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) logging::OuterDocDestroy(this); +#endif + + if (auto* bridge = dom::BrowserBridgeChild::GetFrom(mContent)) { + uint64_t id = reinterpret_cast<uintptr_t>(UniqueID()); + if (bridge->GetEmbedderAccessibleID() == id) { + // We were the last embedder accessible sent via PBrowserBridge; i.e. a + // new embedder accessible hasn't been created yet for this iframe. Clear + // the embedder accessible on PBrowserBridge. + bridge->SetEmbedderAccessible(nullptr, 0); + } + } + + LocalAccessible* child = mChildren.SafeElementAt(0, nullptr); + if (child) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) { + logging::DocDestroy("outerdoc's child document rebind is scheduled", + child->AsDoc()->DocumentNode()); + } +#endif + RemoveChild(child); + + // XXX: sometimes outerdoc accessible is shutdown because of layout style + // change however the presshell of underlying document isn't destroyed and + // the document doesn't get pagehide events. Schedule a document rebind + // to its parent document. Otherwise a document accessible may be lost if + // its outerdoc has being recreated (see bug 862863 for details). + if (!mDoc->IsDefunct()) { + MOZ_ASSERT(!child->IsDefunct(), + "Attempt to reattach shutdown document accessible"); + if (!child->IsDefunct()) { + mDoc->BindChildDocument(child->AsDoc()); + } + } + } + + AccessibleWrap::Shutdown(); +} + +bool OuterDocAccessible::InsertChildAt(uint32_t aIdx, + LocalAccessible* aAccessible) { + MOZ_RELEASE_ASSERT(aAccessible->IsDoc(), + "OuterDocAccessible can have a document child only!"); + + // We keep showing the old document for a bit after creating the new one, + // and while building the new DOM and frame tree. That's done on purpose + // to avoid weird flashes of default background color. + // The old viewer will be destroyed after the new one is created. + // For a11y, it should be safe to shut down the old document now. + if (mChildren.Length()) mChildren[0]->Shutdown(); + + if (!AccessibleWrap::InsertChildAt(0, aAccessible)) return false; + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) { + logging::DocCreate("append document to outerdoc", + aAccessible->AsDoc()->DocumentNode()); + logging::Address("outerdoc", this); + } +#endif + + return true; +} + +bool OuterDocAccessible::RemoveChild(LocalAccessible* aAccessible) { + LocalAccessible* child = mChildren.SafeElementAt(0, nullptr); + MOZ_ASSERT(child == aAccessible, "Wrong child to remove!"); + if (child != aAccessible) { + return false; + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) { + logging::DocDestroy("remove document from outerdoc", + child->AsDoc()->DocumentNode(), child->AsDoc()); + logging::Address("outerdoc", this); + } +#endif + + bool wasRemoved = AccessibleWrap::RemoveChild(child); + + NS_ASSERTION(!mChildren.Length(), + "This child document of outerdoc accessible wasn't removed!"); + + return wasRemoved; +} + +bool OuterDocAccessible::IsAcceptableChild(nsIContent* aEl) const { + // outer document accessible doesn't not participate in ordinal tree + // mutations. + return false; +} + +// Accessible + +uint32_t OuterDocAccessible::ChildCount() const { + uint32_t result = mChildren.Length(); + if (!result && RemoteChildDoc()) { + result = 1; + } + return result; +} + +Accessible* OuterDocAccessible::ChildAt(uint32_t aIndex) const { + LocalAccessible* result = LocalChildAt(aIndex); + if (result || aIndex) { + return result; + } + + return RemoteChildDoc(); +} + +Accessible* OuterDocAccessible::ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) { + LayoutDeviceIntRect docRect = Bounds(); + if (!docRect.Contains(aX, aY)) return nullptr; + + // Always return the inner doc as direct child accessible unless bounds + // outside of it. + Accessible* child = ChildAt(0); + NS_ENSURE_TRUE(child, nullptr); + + if (aWhichChild == EWhichChildAtPoint::DeepestChild) { + return child->ChildAtPoint(aX, aY, EWhichChildAtPoint::DeepestChild); + } + return child; +} + +DocAccessibleParent* OuterDocAccessible::RemoteChildDoc() const { + dom::BrowserParent* tab = dom::BrowserParent::GetFrom(GetContent()); + if (!tab) { + return nullptr; + } + + return tab->GetTopLevelDocAccessible(); +} diff --git a/accessible/generic/OuterDocAccessible.h b/accessible/generic/OuterDocAccessible.h new file mode 100644 index 0000000000..6c16d69124 --- /dev/null +++ b/accessible/generic/OuterDocAccessible.h @@ -0,0 +1,80 @@ +/* -*- 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_OUTERDOCACCESSIBLE_H_ +#define MOZILLA_A11Y_OUTERDOCACCESSIBLE_H_ + +#include "AccessibleWrap.h" + +namespace mozilla { + +namespace dom { +class BrowserBridgeChild; +} + +namespace a11y { +class DocAccessibleParent; + +/** + * Used for <browser>, <frame>, <iframe>, <page> or editor> elements. + * + * In these variable names, "outer" relates to the OuterDocAccessible as + * opposed to the DocAccessibleWrap which is "inner". The outer node is + * a something like tags listed above, whereas the inner node corresponds to + * the inner document root. + */ + +class OuterDocAccessible final : public AccessibleWrap { + public: + OuterDocAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(OuterDocAccessible, AccessibleWrap) + + DocAccessibleParent* RemoteChildDoc() const; + + /** + * For iframes in a content process which will be rendered in another content + * process, tell the parent process about this OuterDocAccessible + * so it can link the trees together when the embedded document is added. + * Note that an OuterDocAccessible can be created before the + * BrowserBridgeChild or vice versa. Therefore, this must be conditionally + * called when either of these is created. + */ + void SendEmbedderAccessible(dom::BrowserBridgeChild* aBridge); + + Maybe<nsMargin> GetCrossDocOffset() { return mCrossDocOffset; } + + void SetCrossDocOffset(nsMargin aMargin) { mCrossDocOffset = Some(aMargin); } + + // LocalAccessible + virtual void Shutdown() override; + virtual mozilla::a11y::role NativeRole() const override; + virtual LocalAccessible* LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override; + + virtual bool InsertChildAt(uint32_t aIdx, LocalAccessible* aChild) override; + virtual bool RemoveChild(LocalAccessible* aAccessible) override; + virtual bool IsAcceptableChild(nsIContent* aEl) const override; + + virtual uint32_t ChildCount() const override; + + // Accessible + virtual Accessible* ChildAt(uint32_t aIndex) const override; + virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) override; + + protected: + virtual ~OuterDocAccessible() override; + Maybe<nsMargin> mCrossDocOffset; +}; + +inline OuterDocAccessible* LocalAccessible::AsOuterDoc() { + return IsOuterDoc() ? static_cast<OuterDocAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/RootAccessible.cpp b/accessible/generic/RootAccessible.cpp new file mode 100644 index 0000000000..b41e348912 --- /dev/null +++ b/accessible/generic/RootAccessible.cpp @@ -0,0 +1,709 @@ +/* -*- 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 "RootAccessible.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/PresShell.h" // for nsAccUtils::GetDocAccessibleFor() +#include "nsXULPopupManager.h" + +#define CreateEvent CreateEventA + +#include "LocalAccessible-inl.h" +#include "DocAccessible-inl.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "nsEventShell.h" +#include "Relation.h" +#include "Role.h" +#include "States.h" +#include "XULTreeAccessible.h" + +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/CustomEvent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/BrowserHost.h" + +#include "nsIDocShellTreeOwner.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "nsIDOMXULMultSelectCntrlEl.h" +#include "mozilla/dom/Document.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPropertyBag2.h" +#include "nsPIDOMWindow.h" +#include "nsIWebBrowserChrome.h" +#include "nsReadableUtils.h" +#include "nsFocusManager.h" +#include "nsGlobalWindow.h" + +#include "nsIAppWindow.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_ISUPPORTS_INHERITED(RootAccessible, DocAccessible, nsIDOMEventListener) + +//////////////////////////////////////////////////////////////////////////////// +// Constructor/destructor + +RootAccessible::RootAccessible(Document* aDocument, PresShell* aPresShell) + : DocAccessibleWrap(aDocument, aPresShell) { + mType = eRootType; +} + +RootAccessible::~RootAccessible() {} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +ENameValueFlag RootAccessible::Name(nsString& aName) const { + aName.Truncate(); + + if (ARIARoleMap()) { + LocalAccessible::Name(aName); + if (!aName.IsEmpty()) return eNameOK; + } + + mDocumentNode->GetTitle(aName); + return eNameOK; +} + +// RootAccessible protected member +uint32_t RootAccessible::GetChromeFlags() const { + // Return the flag set for the top level window as defined + // by nsIWebBrowserChrome::CHROME_WINDOW_[FLAGNAME] + // Not simple: nsIAppWindow is not just a QI from nsIDOMWindow + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode); + NS_ENSURE_TRUE(docShell, 0); + nsCOMPtr<nsIDocShellTreeOwner> treeOwner; + docShell->GetTreeOwner(getter_AddRefs(treeOwner)); + NS_ENSURE_TRUE(treeOwner, 0); + nsCOMPtr<nsIAppWindow> appWin(do_GetInterface(treeOwner)); + if (!appWin) { + return 0; + } + uint32_t chromeFlags; + appWin->GetChromeFlags(&chromeFlags); + return chromeFlags; +} + +uint64_t RootAccessible::NativeState() const { + uint64_t state = DocAccessibleWrap::NativeState(); + if (state & states::DEFUNCT) return state; + + uint32_t chromeFlags = GetChromeFlags(); + if (chromeFlags & nsIWebBrowserChrome::CHROME_WINDOW_RESIZE) { + state |= states::SIZEABLE; + } + // If it has a titlebar it's movable + // XXX unless it's minimized or maximized, but not sure + // how to detect that + if (chromeFlags & nsIWebBrowserChrome::CHROME_TITLEBAR) { + state |= states::MOVEABLE; + } + if (chromeFlags & nsIWebBrowserChrome::CHROME_MODAL) state |= states::MODAL; + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm && fm->GetActiveWindow() == mDocumentNode->GetWindow()) { + state |= states::ACTIVE; + } + + return state; +} + +const char* const kEventTypes[] = { +#ifdef DEBUG_DRAGDROPSTART + // Capture mouse over events and fire fake DRAGDROPSTART event to simplify + // debugging a11y objects with event viewers. + "mouseover", +#endif + // Fired when list or tree selection changes. + "select", + // Fired when value changes immediately, wether or not focused changed. + "ValueChange", "AlertActive", "TreeRowCountChanged", "TreeInvalidated", + // add ourself as a OpenStateChange listener (custom event fired in + // tree.xml) + "OpenStateChange", + // add ourself as a CheckboxStateChange listener (custom event fired in + // HTMLInputElement.cpp) + "CheckboxStateChange", + // add ourself as a RadioStateChange Listener (custom event fired in in + // HTMLInputElement.cpp & radio.js) + "RadioStateChange", "popupshown", "popuphiding", "DOMMenuInactive", + "DOMMenuItemActive", "DOMMenuItemInactive", "DOMMenuBarActive", + "DOMMenuBarInactive", "scroll", "DOMTitleChanged"}; + +nsresult RootAccessible::AddEventListeners() { + // EventTarget interface allows to register event listeners to + // receive untrusted events (synthetic events generated by untrusted code). + // For example, XBL bindings implementations for elements that are hosted in + // non chrome document fire untrusted events. + // We must use the window's parent target in order to receive events from + // iframes and shadow DOM; e.g. ValueChange events from a <select> in an + // iframe or shadow DOM. The root document itself doesn't receive these. + nsPIDOMWindowOuter* window = mDocumentNode->GetWindow(); + nsCOMPtr<EventTarget> nstarget = window ? window->GetParentTarget() : nullptr; + + if (nstarget) { + for (const char *const *e = kEventTypes, *const *e_end = + ArrayEnd(kEventTypes); + e < e_end; ++e) { + nsresult rv = nstarget->AddEventListener(NS_ConvertASCIItoUTF16(*e), this, + true, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return DocAccessible::AddEventListeners(); +} + +nsresult RootAccessible::RemoveEventListeners() { + nsPIDOMWindowOuter* window = mDocumentNode->GetWindow(); + nsCOMPtr<EventTarget> target = window ? window->GetParentTarget() : nullptr; + if (target) { + for (const char *const *e = kEventTypes, *const *e_end = + ArrayEnd(kEventTypes); + e < e_end; ++e) { + target->RemoveEventListener(NS_ConvertASCIItoUTF16(*e), this, true); + } + } + + // Do this before removing clearing caret accessible, so that it can use + // shutdown the caret accessible's selection listener + DocAccessible::RemoveEventListeners(); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// public + +void RootAccessible::DocumentActivated(DocAccessible* aDocument) {} + +//////////////////////////////////////////////////////////////////////////////// +// nsIDOMEventListener + +NS_IMETHODIMP +RootAccessible::HandleEvent(Event* aDOMEvent) { + MOZ_ASSERT(aDOMEvent); + if (IsDefunct()) { + // Even though we've been shut down, RemoveEventListeners might not have + // removed the event handlers on the window's parent target if GetWindow + // returned null, so we might still get events here in this case. We should + // just ignore these events. + return NS_OK; + } + + nsCOMPtr<nsINode> origTargetNode = + do_QueryInterface(aDOMEvent->GetOriginalTarget()); + if (!origTargetNode) return NS_OK; + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDOMEvents)) { + nsAutoString eventType; + aDOMEvent->GetType(eventType); + logging::DOMEvent("handled", origTargetNode, eventType); + } +#endif + + DocAccessible* document = + GetAccService()->GetDocAccessible(origTargetNode->OwnerDoc()); + + if (document) { + nsAutoString eventType; + aDOMEvent->GetType(eventType); + if (eventType.EqualsLiteral("scroll")) { + // We don't put this in the notification queue for 2 reasons: + // 1. We will flood the queue with repetitive events. + // 2. Since this doesn't necessarily touch layout, we are not + // guaranteed to have a WillRefresh tick any time soon. + document->HandleScroll(origTargetNode); + } else { + // Root accessible exists longer than any of its descendant documents so + // that we are guaranteed notification is processed before root accessible + // is destroyed. + // For shadow DOM, GetOriginalTarget on the Event returns null if we + // process the event async, so we must pass the target node as well. + document->HandleNotification<RootAccessible, Event, nsINode>( + this, &RootAccessible::ProcessDOMEvent, aDOMEvent, origTargetNode); + } + } + + return NS_OK; +} + +// RootAccessible protected +void RootAccessible::ProcessDOMEvent(Event* aDOMEvent, nsINode* aTarget) { + MOZ_ASSERT(aDOMEvent); + MOZ_ASSERT(aTarget); + + nsAutoString eventType; + aDOMEvent->GetType(eventType); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDOMEvents)) { + logging::DOMEvent("processed", aTarget, eventType); + } +#endif + + if (eventType.EqualsLiteral("popuphiding")) { + HandlePopupHidingEvent(aTarget); + return; + } + + DocAccessible* targetDocument = + GetAccService()->GetDocAccessible(aTarget->OwnerDoc()); + if (!targetDocument) { + // Document has ceased to exist. + return; + } + + if (eventType.EqualsLiteral("popupshown") && + aTarget->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) { + targetDocument->ContentInserted(aTarget->AsContent(), + aTarget->GetNextSibling()); + return; + } + + LocalAccessible* accessible = + targetDocument->GetAccessibleOrContainer(aTarget); + if (!accessible) return; + + if (accessible->IsDoc() && eventType.EqualsLiteral("DOMTitleChanged")) { + targetDocument->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, + accessible); + return; + } + + XULTreeAccessible* treeAcc = accessible->AsXULTree(); + if (treeAcc) { + if (eventType.EqualsLiteral("TreeRowCountChanged")) { + HandleTreeRowCountChangedEvent(aDOMEvent, treeAcc); + return; + } + + if (eventType.EqualsLiteral("TreeInvalidated")) { + HandleTreeInvalidatedEvent(aDOMEvent, treeAcc); + return; + } + } + + if (eventType.EqualsLiteral("RadioStateChange")) { + uint64_t state = accessible->State(); + bool isEnabled = (state & (states::CHECKED | states::SELECTED)) != 0; + + if (accessible->NeedsDOMUIEvent()) { + RefPtr<AccEvent> accEvent = + new AccStateChangeEvent(accessible, states::CHECKED, isEnabled); + nsEventShell::FireEvent(accEvent); + } + + if (isEnabled) { + FocusMgr()->ActiveItemChanged(accessible); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("RadioStateChange", accessible); + } +#endif + } + + return; + } + + if (eventType.EqualsLiteral("CheckboxStateChange")) { + if (accessible->NeedsDOMUIEvent()) { + uint64_t state = accessible->State(); + bool isEnabled = !!(state & states::CHECKED); + + RefPtr<AccEvent> accEvent = + new AccStateChangeEvent(accessible, states::CHECKED, isEnabled); + nsEventShell::FireEvent(accEvent); + } + return; + } + + LocalAccessible* treeItemAcc = nullptr; + // If it's a tree element, need the currently selected item. + if (treeAcc) { + treeItemAcc = accessible->CurrentItem(); + if (treeItemAcc) accessible = treeItemAcc; + } + + if (treeItemAcc && eventType.EqualsLiteral("OpenStateChange")) { + uint64_t state = accessible->State(); + bool isEnabled = (state & states::EXPANDED) != 0; + + RefPtr<AccEvent> accEvent = + new AccStateChangeEvent(accessible, states::EXPANDED, isEnabled); + nsEventShell::FireEvent(accEvent); + return; + } + + nsINode* targetNode = accessible->GetNode(); + if (treeItemAcc && eventType.EqualsLiteral("select")) { + // XXX: We shouldn't be based on DOM select event which doesn't provide us + // any context info. We should integrate into nsTreeSelection instead. + // If multiselect tree, we should fire selectionadd or selection removed + if (FocusMgr()->HasDOMFocus(targetNode)) { + nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSel = + targetNode->AsElement()->AsXULMultiSelectControl(); + if (!multiSel) { + // This shouldn't be possible. All XUL trees should have + // nsIDOMXULMultiSelectControlElement, and the tree is focused, so it + // shouldn't be dying. Nevertheless, this sometimes happens in the wild + // (bug 1597043). + MOZ_ASSERT_UNREACHABLE( + "XUL tree doesn't have nsIDOMXULMultiSelectControlElement"); + return; + } + nsAutoString selType; + multiSel->GetSelType(selType); + if (selType.IsEmpty() || !selType.EqualsLiteral("single")) { + // XXX: We need to fire EVENT_SELECTION_ADD and EVENT_SELECTION_REMOVE + // for each tree item. Perhaps each tree item will need to cache its + // selection state and fire an event after a DOM "select" event when + // that state changes. XULTreeAccessible::UpdateTreeSelection(); + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN, + accessible); + return; + } + + RefPtr<AccSelChangeEvent> selChangeEvent = new AccSelChangeEvent( + treeAcc, treeItemAcc, AccSelChangeEvent::eSelectionAdd); + nsEventShell::FireEvent(selChangeEvent); + return; + } + } else if (eventType.EqualsLiteral("AlertActive")) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_ALERT, accessible); + } else if (eventType.EqualsLiteral("popupshown")) { + HandlePopupShownEvent(accessible); + } else if (eventType.EqualsLiteral("DOMMenuInactive")) { + if (accessible->Role() == roles::MENUPOPUP) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END, + accessible); + } + if (auto* focus = FocusMgr()->FocusedLocalAccessible()) { + // Intentionally use the content tree, because Linux strips menupopups + // from the a11y tree so accessible might be an arbitrary ancestor. + if (focus->GetContent() && + focus->GetContent()->IsShadowIncludingInclusiveDescendantOf( + aTarget)) { + // Move the focus to the topmost menu active content if any. The + // menu item in the parent menu will not fire a DOMMenuItemActive + // event if it's already active. + LocalAccessible* newActiveAccessible = nullptr; + if (auto* pm = nsXULPopupManager::GetInstance()) { + if (auto* content = pm->GetTopActiveMenuItemContent()) { + newActiveAccessible = + accessible->Document()->GetAccessible(content); + } + } + FocusMgr()->ActiveItemChanged(newActiveAccessible); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuInactive", + newActiveAccessible); + } +#endif + } + } + } else if (eventType.EqualsLiteral("DOMMenuItemActive")) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::ACTIVE, true); + nsEventShell::FireEvent(event); + FocusMgr()->ActiveItemChanged(accessible); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuItemActive", accessible); + } +#endif + } else if (eventType.EqualsLiteral("DOMMenuItemInactive")) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::ACTIVE, false); + nsEventShell::FireEvent(event); + + // Process DOMMenuItemInactive event for autocomplete only because this is + // unique widget that may acquire focus from autocomplete popup while popup + // stays open and has no active item. In case of XUL tree autocomplete + // popup this event is fired for tree accessible. + LocalAccessible* widget = + accessible->IsWidget() ? accessible : accessible->ContainerWidget(); + if (widget && widget->IsAutoCompletePopup()) { + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuItemInactive", accessible); + } +#endif + } + } else if (eventType.EqualsLiteral( + "DOMMenuBarActive")) { // Always from user input + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENU_START, accessible, + eFromUserInput); + + // Notify of active item change when menubar gets active and if it has + // current item. This is a case of mouseover (set current menuitem) and + // mouse click (activate the menubar). If menubar doesn't have current item + // (can be a case of menubar activation from keyboard) then ignore this + // notification because later we'll receive DOMMenuItemActive event after + // current menuitem is set. + LocalAccessible* activeItem = accessible->CurrentItem(); + if (activeItem) { + FocusMgr()->ActiveItemChanged(activeItem); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuBarActive", accessible); + } +#endif + } + } else if (eventType.EqualsLiteral( + "DOMMenuBarInactive")) { // Always from user input + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENU_END, accessible, + eFromUserInput); + + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuBarInactive", accessible); + } +#endif + } else if (accessible->NeedsDOMUIEvent() && + eventType.EqualsLiteral("ValueChange")) { + uint32_t event = accessible->HasNumericValue() + ? nsIAccessibleEvent::EVENT_VALUE_CHANGE + : nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE; + targetDocument->FireDelayedEvent(event, accessible); + } +#ifdef DEBUG_DRAGDROPSTART + else if (eventType.EqualsLiteral("mouseover")) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_DRAGDROP_START, + accessible); + } +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +void RootAccessible::Shutdown() { + // Called manually or by LocalAccessible::LastRelease() + if (HasShutdown()) { + return; + } + DocAccessibleWrap::Shutdown(); +} + +Relation RootAccessible::RelationByType(RelationType aType) const { + if (!mDocumentNode || aType != RelationType::EMBEDS) { + return DocAccessibleWrap::RelationByType(aType); + } + + if (RemoteAccessible* remoteDoc = GetPrimaryRemoteTopLevelContentDoc()) { + return Relation(remoteDoc); + } + + if (nsIDocShell* docShell = mDocumentNode->GetDocShell()) { + nsCOMPtr<nsIDocShellTreeOwner> owner; + docShell->GetTreeOwner(getter_AddRefs(owner)); + if (owner) { + nsCOMPtr<nsIDocShellTreeItem> contentShell; + owner->GetPrimaryContentShell(getter_AddRefs(contentShell)); + if (contentShell) { + return Relation(nsAccUtils::GetDocAccessibleFor(contentShell)); + } + } + } + + return Relation(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Protected members + +void RootAccessible::HandlePopupShownEvent(LocalAccessible* aAccessible) { + roles::Role role = aAccessible->Role(); + + if (role == roles::MENUPOPUP) { + // Don't fire menupopup events for combobox and autocomplete lists. + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, + aAccessible); + return; + } + + if (role == roles::COMBOBOX_LIST) { + // Fire expanded state change event for comboboxes and autocompeletes. + LocalAccessible* combobox = aAccessible->LocalParent(); + if (!combobox) return; + + if (combobox->IsCombobox()) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(combobox, states::EXPANDED, true); + nsEventShell::FireEvent(event); + } + + // If aria-activedescendant is present, redirect focus. + // This is needed for parent process <select> dropdowns, which use a + // menulist containing div elements instead of XUL menuitems. XUL menuitems + // fire DOMMenuItemActive events from layout instead. + MOZ_ASSERT(aAccessible->Elm()); + if (aAccessible->Elm()->HasAttr(nsGkAtoms::aria_activedescendant)) { + LocalAccessible* activeDescendant = aAccessible->CurrentItem(); + if (activeDescendant) { + FocusMgr()->ActiveItemChanged(activeDescendant, false); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("ARIA activedescendant on popup", + activeDescendant); + } +#endif + } + } + } +} + +void RootAccessible::HandlePopupHidingEvent(nsINode* aPopupNode) { + DocAccessible* document = nsAccUtils::GetDocAccessibleFor(aPopupNode); + if (!document) { + return; + } + + if (aPopupNode->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) { + document->ContentRemoved(aPopupNode->AsContent()); + return; + } + + // Get popup accessible. There are cases when popup element isn't accessible + // but an underlying widget is and behaves like popup, an example is + // autocomplete popups. + LocalAccessible* popup = document->GetAccessible(aPopupNode); + if (!popup) { + LocalAccessible* popupContainer = + document->GetContainerAccessible(aPopupNode); + if (!popupContainer) { + return; + } + + uint32_t childCount = popupContainer->ChildCount(); + for (uint32_t idx = 0; idx < childCount; idx++) { + LocalAccessible* child = popupContainer->LocalChildAt(idx); + if (child->IsAutoCompletePopup()) { + popup = child; + break; + } + } + + // No popup no events. Focus is managed by DOM. This is a case for + // menupopups of menus on Linux since there are no accessible for popups. + if (!popup) { + return; + } + } + + // In case of autocompletes and comboboxes fire state change event for + // expanded state. Note, HTML form autocomplete isn't a subject of state + // change event because they aren't autocompletes strictly speaking. + + // HTML select is target of popuphidding event. Otherwise get container + // widget. No container widget means this is either tooltip or menupopup. + // No events in the former case. + LocalAccessible* widget = nullptr; + if (popup->IsCombobox()) { + widget = popup; + } else { + widget = popup->ContainerWidget(); + if (!widget) { + if (!popup->IsMenuPopup()) { + return; + } + widget = popup; + } + } + + // Fire expanded state change event. + if (widget->IsCombobox()) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(widget, states::EXPANDED, false); + document->FireDelayedEvent(event); + } +} + +static void GetPropertyBagFromEvent(Event* aEvent, + nsIPropertyBag2** aPropertyBag) { + *aPropertyBag = nullptr; + + CustomEvent* customEvent = aEvent->AsCustomEvent(); + if (!customEvent) return; + + AutoJSAPI jsapi; + if (!jsapi.Init(customEvent->GetParentObject())) return; + + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> detail(cx); + customEvent->GetDetail(cx, &detail); + if (!detail.isObject()) return; + + JS::Rooted<JSObject*> detailObj(cx, &detail.toObject()); + + nsresult rv; + nsCOMPtr<nsIPropertyBag2> propBag; + rv = UnwrapArg<nsIPropertyBag2>(cx, detailObj, getter_AddRefs(propBag)); + if (NS_FAILED(rv)) return; + + propBag.forget(aPropertyBag); +} + +void RootAccessible::HandleTreeRowCountChangedEvent( + Event* aEvent, XULTreeAccessible* aAccessible) { + nsCOMPtr<nsIPropertyBag2> propBag; + GetPropertyBagFromEvent(aEvent, getter_AddRefs(propBag)); + if (!propBag) return; + + nsresult rv; + int32_t index, count; + rv = propBag->GetPropertyAsInt32(u"index"_ns, &index); + if (NS_FAILED(rv)) return; + + rv = propBag->GetPropertyAsInt32(u"count"_ns, &count); + if (NS_FAILED(rv)) return; + + aAccessible->InvalidateCache(index, count); +} + +void RootAccessible::HandleTreeInvalidatedEvent( + Event* aEvent, XULTreeAccessible* aAccessible) { + nsCOMPtr<nsIPropertyBag2> propBag; + GetPropertyBagFromEvent(aEvent, getter_AddRefs(propBag)); + if (!propBag) return; + + int32_t startRow = 0, endRow = -1, startCol = 0, endCol = -1; + propBag->GetPropertyAsInt32(u"startrow"_ns, &startRow); + propBag->GetPropertyAsInt32(u"endrow"_ns, &endRow); + propBag->GetPropertyAsInt32(u"startcolumn"_ns, &startCol); + propBag->GetPropertyAsInt32(u"endcolumn"_ns, &endCol); + + aAccessible->TreeViewInvalidated(startRow, endRow, startCol, endCol); +} + +RemoteAccessible* RootAccessible::GetPrimaryRemoteTopLevelContentDoc() const { + nsCOMPtr<nsIDocShellTreeOwner> owner; + mDocumentNode->GetDocShell()->GetTreeOwner(getter_AddRefs(owner)); + NS_ENSURE_TRUE(owner, nullptr); + + nsCOMPtr<nsIRemoteTab> remoteTab; + owner->GetPrimaryRemoteTab(getter_AddRefs(remoteTab)); + if (!remoteTab) { + return nullptr; + } + + auto tab = static_cast<dom::BrowserHost*>(remoteTab.get()); + return tab->GetTopLevelDocAccessible(); +} diff --git a/accessible/generic/RootAccessible.h b/accessible/generic/RootAccessible.h new file mode 100644 index 0000000000..b1e7e42fdb --- /dev/null +++ b/accessible/generic/RootAccessible.h @@ -0,0 +1,93 @@ +/* -*- 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_RootAccessible_h__ +#define mozilla_a11y_RootAccessible_h__ + +#include "HyperTextAccessible.h" +#include "DocAccessibleWrap.h" + +#include "nsIDOMEventListener.h" + +namespace mozilla { + +class PresShell; + +namespace a11y { + +/** + * The node at a root of the accessibility tree. This node originated in the + * current process. If this is the parent process, RootAccessible is the + * Accessible for the top-level window. If this is a content process, + * RootAccessible is a top-level content document in this process, which is + * either a tab document or an out-of-process iframe. + */ +class RootAccessible : public DocAccessibleWrap, public nsIDOMEventListener { + NS_DECL_ISUPPORTS_INHERITED + + public: + RootAccessible(dom::Document* aDocument, PresShell* aPresShell); + + // nsIDOMEventListener + NS_DECL_NSIDOMEVENTLISTENER + + // LocalAccessible + virtual void Shutdown() override; + virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) const override; + virtual Relation RelationByType(RelationType aType) const override; + virtual uint64_t NativeState() const override; + + // RootAccessible + + /** + * Notify that the sub document presshell was activated. + */ + virtual void DocumentActivated(DocAccessible* aDocument); + + /** + * Return the primary remote top level document if any. + */ + RemoteAccessible* GetPrimaryRemoteTopLevelContentDoc() const; + + protected: + virtual ~RootAccessible(); + + /** + * Add/remove DOM event listeners. + */ + virtual nsresult AddEventListeners() override; + virtual nsresult RemoveEventListeners() override; + + /** + * Process the DOM event. + */ + void ProcessDOMEvent(dom::Event* aDOMEvent, nsINode* aTarget); + + /** + * Process "popupshown" event. Used by HandleEvent(). + */ + void HandlePopupShownEvent(LocalAccessible* aAccessible); + + /* + * Process "popuphiding" event. Used by HandleEvent(). + */ + void HandlePopupHidingEvent(nsINode* aNode); + + void HandleTreeRowCountChangedEvent(dom::Event* aEvent, + XULTreeAccessible* aAccessible); + void HandleTreeInvalidatedEvent(dom::Event* aEvent, + XULTreeAccessible* aAccessible); + + uint32_t GetChromeFlags() const; +}; + +inline RootAccessible* LocalAccessible::AsRoot() { + return IsRoot() ? static_cast<mozilla::a11y::RootAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/TableAccessible.cpp b/accessible/generic/TableAccessible.cpp new file mode 100644 index 0000000000..9bcbfdcc2f --- /dev/null +++ b/accessible/generic/TableAccessible.cpp @@ -0,0 +1,317 @@ +/* -*- 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 "TableAccessible.h" + +#include "LocalAccessible-inl.h" +#include "AccIterator.h" + +#include "nsTableCellFrame.h" +#include "nsTableWrapperFrame.h" +#include "TableCellAccessible.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +bool TableAccessible::IsProbablyLayoutTable() { + // Implement a heuristic to determine if table is most likely used for layout. + + // XXX do we want to look for rowspan or colspan, especialy that span all but + // a couple cells at the beginning or end of a row/col, and especially when + // they occur at the edge of a table? + + // XXX For now debugging descriptions are always on via SHOW_LAYOUT_HEURISTIC + // This will allow release trunk builds to be used by testers to refine + // the algorithm. Integrate it into Logging. + // Change to |#define SHOW_LAYOUT_HEURISTIC DEBUG| before final release +#ifdef SHOW_LAYOUT_HEURISTIC +# define RETURN_LAYOUT_ANSWER(isLayout, heuristic) \ + { \ + mLayoutHeuristic = isLayout \ + ? nsLiteralString(u"layout table: " heuristic) \ + : nsLiteralString(u"data table: " heuristic); \ + return isLayout; \ + } +#else +# define RETURN_LAYOUT_ANSWER(isLayout, heuristic) \ + { return isLayout; } +#endif + + LocalAccessible* thisacc = AsAccessible(); + + MOZ_ASSERT(!thisacc->IsDefunct(), "Table accessible should not be defunct"); + + // Need to see all elements while document is being edited. + if (thisacc->Document()->State() & states::EDITABLE) { + RETURN_LAYOUT_ANSWER(false, "In editable document"); + } + + // Check to see if an ARIA role overrides the role from native markup, + // but for which we still expose table semantics (treegrid, for example). + if (thisacc->HasARIARole()) { + RETURN_LAYOUT_ANSWER(false, "Has role attribute"); + } + + dom::Element* el = thisacc->Elm(); + if (el->IsMathMLElement(nsGkAtoms::mtable_)) { + RETURN_LAYOUT_ANSWER(false, "MathML matrix"); + } + + MOZ_ASSERT(el->IsHTMLElement(nsGkAtoms::table), + "Table should not be built by CSS display:table style"); + + // Check if datatable attribute has "0" value. + if (el->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datatable, u"0"_ns, + eCaseMatters)) { + RETURN_LAYOUT_ANSWER(true, "Has datatable = 0 attribute, it's for layout"); + } + + // Check for legitimate data table attributes. + if (el->Element::HasNonEmptyAttr(nsGkAtoms::summary)) { + RETURN_LAYOUT_ANSWER(false, "Has summary -- legitimate table structures"); + } + + // Check for legitimate data table elements. + LocalAccessible* caption = thisacc->LocalFirstChild(); + if (caption && caption->IsHTMLCaption() && caption->HasChildren()) { + RETURN_LAYOUT_ANSWER(false, + "Not empty caption -- legitimate table structures"); + } + + for (nsIContent* childElm = el->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (!childElm->IsHTMLElement()) continue; + + if (childElm->IsAnyOfHTMLElements(nsGkAtoms::col, nsGkAtoms::colgroup, + nsGkAtoms::tfoot, nsGkAtoms::thead)) { + RETURN_LAYOUT_ANSWER( + false, + "Has col, colgroup, tfoot or thead -- legitimate table structures"); + } + + if (childElm->IsHTMLElement(nsGkAtoms::tbody)) { + for (nsIContent* rowElm = childElm->GetFirstChild(); rowElm; + rowElm = rowElm->GetNextSibling()) { + if (rowElm->IsHTMLElement(nsGkAtoms::tr)) { + if (LocalAccessible* row = + thisacc->Document()->GetAccessible(rowElm)) { + if (const nsRoleMapEntry* roleMapEntry = row->ARIARoleMap()) { + if (roleMapEntry->role != roles::ROW) { + RETURN_LAYOUT_ANSWER(true, "Repurposed tr with different role"); + } + } + } + + for (nsIContent* cellElm = rowElm->GetFirstChild(); cellElm; + cellElm = cellElm->GetNextSibling()) { + if (cellElm->IsHTMLElement()) { + if (cellElm->NodeInfo()->Equals(nsGkAtoms::th)) { + RETURN_LAYOUT_ANSWER(false, + "Has th -- legitimate table structures"); + } + + if (cellElm->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::headers) || + cellElm->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::scope) || + cellElm->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::abbr)) { + RETURN_LAYOUT_ANSWER(false, + "Has headers, scope, or abbr attribute -- " + "legitimate table structures"); + } + + if (LocalAccessible* cell = + thisacc->Document()->GetAccessible(cellElm)) { + if (const nsRoleMapEntry* roleMapEntry = cell->ARIARoleMap()) { + if (roleMapEntry->role != roles::CELL && + roleMapEntry->role != roles::COLUMNHEADER && + roleMapEntry->role != roles::ROWHEADER && + roleMapEntry->role != roles::GRID_CELL) { + RETURN_LAYOUT_ANSWER(true, + "Repurposed cell with different role"); + } + } + if (cell->ChildCount() == 1 && + cell->LocalFirstChild()->IsAbbreviation()) { + RETURN_LAYOUT_ANSWER( + false, "has abbr -- legitimate table structures"); + } + } + } + } + } + } + } + } + + // Check for nested tables. + nsCOMPtr<nsIHTMLCollection> nestedTables = + el->GetElementsByTagName(u"table"_ns); + if (nestedTables->Length() > 0) { + RETURN_LAYOUT_ANSWER(true, "Has a nested table within it"); + } + + // If only 1 column or only 1 row, it's for layout. + auto colCount = ColCount(); + if (colCount <= 1) { + RETURN_LAYOUT_ANSWER(true, "Has only 1 column"); + } + auto rowCount = RowCount(); + if (rowCount <= 1) { + RETURN_LAYOUT_ANSWER(true, "Has only 1 row"); + } + + // Check for many columns. + if (colCount >= 5) { + RETURN_LAYOUT_ANSWER(false, ">=5 columns"); + } + + // Now we know there are 2-4 columns and 2 or more rows. Check to see if + // there are visible borders on the cells. + // XXX currently, we just check the first cell -- do we really need to do + // more? + nsTableWrapperFrame* tableFrame = do_QueryFrame(el->GetPrimaryFrame()); + if (!tableFrame) { + RETURN_LAYOUT_ANSWER(false, "table with no frame!"); + } + + nsIFrame* cellFrame = tableFrame->GetCellFrameAt(0, 0); + if (!cellFrame) { + RETURN_LAYOUT_ANSWER(false, "table's first cell has no frame!"); + } + + nsMargin border; + cellFrame->GetXULBorder(border); + if (border.top && border.bottom && border.left && border.right) { + RETURN_LAYOUT_ANSWER(false, "Has nonzero border-width on table cell"); + } + + // Rules for non-bordered tables with 2-4 columns and 2+ rows from here on + // forward. + + // Check for styled background color across rows (alternating background + // color is a common feature for data tables). + auto childCount = thisacc->ChildCount(); + nscolor rowColor = 0; + nscolor prevRowColor; + for (auto childIdx = 0U; childIdx < childCount; childIdx++) { + LocalAccessible* child = thisacc->LocalChildAt(childIdx); + if (child->IsHTMLTableRow()) { + prevRowColor = rowColor; + nsIFrame* rowFrame = child->GetFrame(); + MOZ_ASSERT(rowFrame, "Table hierarchy got screwed up"); + if (!rowFrame) { + RETURN_LAYOUT_ANSWER(false, "Unexpected table hierarchy"); + } + + rowColor = rowFrame->StyleBackground()->BackgroundColor(rowFrame); + + if (childIdx > 0 && prevRowColor != rowColor) { + RETURN_LAYOUT_ANSWER(false, + "2 styles of row background color, non-bordered"); + } + } + } + + // Check for many rows. + const uint32_t kMaxLayoutRows = 20; + if (rowCount > kMaxLayoutRows) { // A ton of rows, this is probably for data + RETURN_LAYOUT_ANSWER(false, ">= kMaxLayoutRows (20) and non-bordered"); + } + + // Check for very wide table. + nsIFrame* documentFrame = thisacc->Document()->GetFrame(); + nsSize documentSize = documentFrame->GetSize(); + if (documentSize.width > 0) { + nsSize tableSize = thisacc->GetFrame()->GetSize(); + int32_t percentageOfDocWidth = (100 * tableSize.width) / documentSize.width; + if (percentageOfDocWidth > 95) { + // 3-4 columns, no borders, not a lot of rows, and 95% of the doc's width + // Probably for layout + RETURN_LAYOUT_ANSWER( + true, "<= 4 columns, table width is 95% of document width"); + } + } + + // Two column rules. + if (rowCount * colCount <= 10) { + RETURN_LAYOUT_ANSWER(true, "2-4 columns, 10 cells or less, non-bordered"); + } + + static const nsLiteralString tags[] = {u"embed"_ns, u"object"_ns, + u"iframe"_ns}; + for (auto& tag : tags) { + nsCOMPtr<nsIHTMLCollection> descendants = el->GetElementsByTagName(tag); + if (descendants->Length() > 0) { + RETURN_LAYOUT_ANSWER(true, + "Has no borders, and has iframe, object or embed, " + "typical of advertisements"); + } + } + + RETURN_LAYOUT_ANSWER(false, + "No layout factor strong enough, so will guess data"); +} + +LocalAccessible* TableAccessible::RowAt(int32_t aRow) { + int32_t rowIdx = aRow; + + AccIterator rowIter(this->AsAccessible(), filters::GetRow); + + LocalAccessible* row = rowIter.Next(); + while (rowIdx != 0 && (row = rowIter.Next())) { + rowIdx--; + } + + return row; +} + +LocalAccessible* TableAccessible::CellInRowAt(LocalAccessible* aRow, + int32_t aColumn) { + int32_t colIdx = aColumn; + + AccIterator cellIter(aRow, filters::GetCell); + LocalAccessible* cell = nullptr; + + while (colIdx >= 0 && (cell = cellIter.Next())) { + MOZ_ASSERT(cell->IsTableCell(), "No table or grid cell!"); + colIdx -= cell->AsTableCell()->ColExtent(); + } + + return cell; +} + +int32_t TableAccessible::ColIndexAt(uint32_t aCellIdx) { + uint32_t colCount = ColCount(); + if (colCount < 1 || aCellIdx >= colCount * RowCount()) { + return -1; // Error: column count is 0 or index out of bounds. + } + + return aCellIdx % colCount; +} + +int32_t TableAccessible::RowIndexAt(uint32_t aCellIdx) { + uint32_t colCount = ColCount(); + if (colCount < 1 || aCellIdx >= colCount * RowCount()) { + return -1; // Error: column count is 0 or index out of bounds. + } + + return aCellIdx / colCount; +} + +void TableAccessible::RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx, + int32_t* aColIdx) { + uint32_t colCount = ColCount(); + if (colCount < 1 || aCellIdx >= colCount * RowCount()) { + *aRowIdx = -1; + *aColIdx = -1; + return; // Error: column count is 0 or index out of bounds. + } + + *aRowIdx = aCellIdx / colCount; + *aColIdx = aCellIdx % colCount; +} diff --git a/accessible/generic/TableAccessible.h b/accessible/generic/TableAccessible.h new file mode 100644 index 0000000000..e2cd51de70 --- /dev/null +++ b/accessible/generic/TableAccessible.h @@ -0,0 +1,72 @@ +/* -*- 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 TABLE_ACCESSIBLE_H +#define TABLE_ACCESSIBLE_H + +#include "LocalAccessible.h" +#include "mozilla/a11y/TableAccessibleBase.h" +#include "mozilla/a11y/TableCellAccessibleBase.h" +#include "nsPointerHashKeys.h" +#include "nsRefPtrHashtable.h" + +namespace mozilla { +namespace a11y { + +/** + * Base class for LocalAccessible table implementations. + */ +class TableAccessible : public TableAccessibleBase { + public: + virtual LocalAccessible* Caption() const override { return nullptr; } + + virtual LocalAccessible* CellAt(uint32_t aRowIdx, uint32_t aColIdx) override { + return nullptr; + } + + virtual int32_t CellIndexAt(uint32_t aRowIdx, uint32_t aColIdx) override { + return ColCount() * aRowIdx + aColIdx; + } + + virtual int32_t ColIndexAt(uint32_t aCellIdx) override; + virtual int32_t RowIndexAt(uint32_t aCellIdx) override; + virtual void RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx, + int32_t* aColIdx) override; + virtual bool IsProbablyLayoutTable() override; + virtual LocalAccessible* AsAccessible() override = 0; + + using HeaderCache = + nsRefPtrHashtable<nsPtrHashKey<const TableCellAccessibleBase>, + LocalAccessible>; + + /** + * Get the header cache, which maps a TableCellAccessible to its previous + * header. + * Although this data is only used in TableCellAccessible, it is stored on + * TableAccessible so the cache can be easily invalidated when the table + * is mutated. + */ + HeaderCache& GetHeaderCache() { return mHeaderCache; } + + protected: + /** + * Return row accessible at the given row index. + */ + LocalAccessible* RowAt(int32_t aRow); + + /** + * Return cell accessible at the given column index in the row. + */ + LocalAccessible* CellInRowAt(LocalAccessible* aRow, int32_t aColumn); + + private: + HeaderCache mHeaderCache; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/TableCellAccessible.cpp b/accessible/generic/TableCellAccessible.cpp new file mode 100644 index 0000000000..c962e2103c --- /dev/null +++ b/accessible/generic/TableCellAccessible.cpp @@ -0,0 +1,165 @@ +/* -*- 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 "TableCellAccessible.h" + +#include "LocalAccessible-inl.h" +#include "TableAccessible.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +void TableCellAccessible::RowHeaderCells(nsTArray<Accessible*>* aCells) { + uint32_t rowIdx = RowIdx(), colIdx = ColIdx(); + TableAccessible* table = Table(); + if (!table) return; + + // Move to the left to find row header cells + for (uint32_t curColIdx = colIdx - 1; curColIdx < colIdx; curColIdx--) { + LocalAccessible* cell = table->CellAt(rowIdx, curColIdx); + if (!cell) continue; + + // CellAt should always return a TableCellAccessible (XXX Bug 587529) + TableCellAccessible* tableCell = cell->AsTableCell(); + NS_ASSERTION(tableCell, "cell should be a table cell!"); + if (!tableCell) continue; + + // Avoid addding cells multiple times, if this cell spans more columns + // we'll get it later. + if (tableCell->ColIdx() == curColIdx && cell->Role() == roles::ROWHEADER) { + aCells->AppendElement(cell); + } + } +} + +LocalAccessible* TableCellAccessible::PrevColHeader() { + TableAccessible* table = Table(); + if (!table) { + return nullptr; + } + + TableAccessible::HeaderCache& cache = table->GetHeaderCache(); + bool inCache = false; + LocalAccessible* cachedHeader = cache.GetWeak(this, &inCache); + if (inCache) { + // Cached but null means we know there is no previous column header. + // if defunct, the cell was removed, so behave as if there is no cached + // value. + if (!cachedHeader || !cachedHeader->IsDefunct()) { + return cachedHeader; + } + } + + uint32_t rowIdx = RowIdx(), colIdx = ColIdx(); + for (uint32_t curRowIdx = rowIdx - 1; curRowIdx < rowIdx; curRowIdx--) { + LocalAccessible* cell = table->CellAt(curRowIdx, colIdx); + if (!cell) { + continue; + } + // CellAt should always return a TableCellAccessible (XXX Bug 587529) + TableCellAccessible* tableCell = cell->AsTableCell(); + MOZ_ASSERT(tableCell, "cell should be a table cell!"); + if (!tableCell) { + continue; + } + + // Check whether the previous table cell has a cached value. + cachedHeader = cache.GetWeak(tableCell, &inCache); + if ( + // We check the cache first because even though we might not use it, + // it's faster than the other conditions. + inCache && + // Only use the cached value if: + // 1. cell is a table cell which is not a column header. In that case, + // cell is the previous header and cachedHeader is the one before that. + // We will return cell later. + cell->Role() != roles::COLUMNHEADER && + // 2. cell starts in this column. If it starts in a previous column and + // extends into this one, its header will be for the starting column, + // which is wrong for this cell. + // ColExtent is faster than ColIdx, so check that first. + (tableCell->ColExtent() == 1 || tableCell->ColIdx() == colIdx)) { + if (!cachedHeader || !cachedHeader->IsDefunct()) { + // Cache it for this cell. + cache.InsertOrUpdate(this, RefPtr<LocalAccessible>(cachedHeader)); + return cachedHeader; + } + } + + // Avoid addding cells multiple times, if this cell spans more rows + // we'll get it later. + if (cell->Role() != roles::COLUMNHEADER || + tableCell->RowIdx() != curRowIdx) { + continue; + } + + // Cache the header we found. + cache.InsertOrUpdate(this, RefPtr<LocalAccessible>(cell)); + return cell; + } + + // There's no header, so cache that fact. + cache.InsertOrUpdate(this, RefPtr<LocalAccessible>(nullptr)); + return nullptr; +} + +void TableCellAccessible::ColHeaderCells(nsTArray<Accessible*>* aCells) { + for (LocalAccessible* cell = PrevColHeader(); cell; + cell = cell->AsTableCell()->PrevColHeader()) { + aCells->AppendElement(cell); + } +} + +a11y::role TableCellAccessible::GetHeaderCellRole( + const LocalAccessible* aAcc) const { + if (!aAcc || !aAcc->GetContent() || !aAcc->GetContent()->IsElement()) { + return roles::NOTHING; + } + + MOZ_ASSERT(aAcc->IsTableCell()); + + // Check value of @scope attribute. + static mozilla::dom::Element::AttrValuesArray scopeValues[] = { + nsGkAtoms::col, nsGkAtoms::colgroup, nsGkAtoms::row, nsGkAtoms::rowgroup, + nullptr}; + int32_t valueIdx = aAcc->GetContent()->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::scope, scopeValues, eCaseMatters); + + switch (valueIdx) { + case 0: + case 1: + return roles::COLUMNHEADER; + case 2: + case 3: + return roles::ROWHEADER; + } + + TableAccessible* table = Table(); + if (!table) { + return roles::NOTHING; + } + + // If the cell next to this one is not a header cell then assume this cell is + // a row header for it. + uint32_t rowIdx = RowIdx(), colIdx = ColIdx(); + LocalAccessible* cell = table->CellAt(rowIdx, colIdx + ColExtent()); + if (cell && !nsCoreUtils::IsHTMLTableHeader(cell->GetContent())) { + return roles::ROWHEADER; + } + + // If the cell below this one is not a header cell then assume this cell is + // a column header for it. + uint32_t rowExtent = RowExtent(); + cell = table->CellAt(rowIdx + rowExtent, colIdx); + if (cell && !nsCoreUtils::IsHTMLTableHeader(cell->GetContent())) { + return roles::COLUMNHEADER; + } + + // Otherwise if this cell is surrounded by header cells only then make a guess + // based on its cell spanning. In other words if it is row spanned then assume + // it's a row header, otherwise it's a column header. + return rowExtent > 1 ? roles::ROWHEADER : roles::COLUMNHEADER; +} diff --git a/accessible/generic/TableCellAccessible.h b/accessible/generic/TableCellAccessible.h new file mode 100644 index 0000000000..3da12dfadb --- /dev/null +++ b/accessible/generic/TableCellAccessible.h @@ -0,0 +1,40 @@ +/* -*- 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_TableCellAccessible_h__ +#define mozilla_a11y_TableCellAccessible_h__ + +#include "mozilla/a11y/TableCellAccessibleBase.h" +#include "TableAccessible.h" + +namespace mozilla { +namespace a11y { + +class LocalAccessible; + +/** + * Base class for LocalAccessible table cell implementations. + */ +class TableCellAccessible : public TableCellAccessibleBase { + public: + virtual TableAccessible* Table() const override = 0; + virtual void ColHeaderCells(nsTArray<Accessible*>* aCells) override; + virtual void RowHeaderCells(nsTArray<Accessible*>* aCells) override; + + protected: + // Get the proper role for the given header cell accessible. The given acc + // must be either an ARIA grid cell accessible for a th element or a true + // table header cell accessible for the result to be valid. + a11y::role GetHeaderCellRole(const LocalAccessible* aAcc) const; + + private: + LocalAccessible* PrevColHeader(); +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_TableCellAccessible_h__ diff --git a/accessible/generic/TextLeafAccessible.cpp b/accessible/generic/TextLeafAccessible.cpp new file mode 100644 index 0000000000..1f7c2ff13a --- /dev/null +++ b/accessible/generic/TextLeafAccessible.cpp @@ -0,0 +1,44 @@ +/* -*- 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 "TextLeafAccessible.h" + +#include "nsAccUtils.h" +#include "DocAccessible.h" +#include "Role.h" + +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// TextLeafAccessible +//////////////////////////////////////////////////////////////////////////////// + +TextLeafAccessible::TextLeafAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : LinkableAccessible(aContent, aDoc) { + mType = eTextLeafType; + mGenericTypes |= eText; + mStateFlags |= eNoKidsFromDOM; +} + +TextLeafAccessible::~TextLeafAccessible() {} + +role TextLeafAccessible::NativeRole() const { + nsIFrame* frame = GetFrame(); + if (frame && frame->IsGeneratedContentFrame()) return roles::STATICTEXT; + + return roles::TEXT_LEAF; +} + +void TextLeafAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset, + uint32_t aLength) { + aText.Append(Substring(mText, aStartOffset, aLength)); +} + +ENameValueFlag TextLeafAccessible::Name(nsString& aName) const { + // Text node, ARIA can't be used. + aName = mText; + return eNameOK; +} diff --git a/accessible/generic/TextLeafAccessible.h b/accessible/generic/TextLeafAccessible.h new file mode 100644 index 0000000000..65d59a2aad --- /dev/null +++ b/accessible/generic/TextLeafAccessible.h @@ -0,0 +1,46 @@ +/* -*- 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_TextLeafAccessible_h__ +#define mozilla_a11y_TextLeafAccessible_h__ + +#include "BaseAccessibles.h" + +namespace mozilla { +namespace a11y { + +/** + * Generic class used for text nodes. + */ +class TextLeafAccessible : public LinkableAccessible { + public: + TextLeafAccessible(nsIContent* aContent, DocAccessible* aDoc); + virtual ~TextLeafAccessible(); + + // LocalAccessible + virtual mozilla::a11y::role NativeRole() const override; + virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0, + uint32_t aLength = UINT32_MAX) override; + virtual ENameValueFlag Name(nsString& aName) const override; + + // TextLeafAccessible + void SetText(const nsAString& aText) { mText = aText; } + const nsString& Text() const { return mText; } + + protected: + nsString mText; +}; + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible downcast method + +inline TextLeafAccessible* LocalAccessible::AsTextLeaf() { + return IsTextLeaf() ? static_cast<TextLeafAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/moz.build b/accessible/generic/moz.build new file mode 100644 index 0000000000..ae4b28e6ba --- /dev/null +++ b/accessible/generic/moz.build @@ -0,0 +1,74 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.a11y += [ + "DocAccessible.h", + "HyperTextAccessible.h", + "LocalAccessible.h", + "OuterDocAccessible.h", +] + +UNIFIED_SOURCES += [ + "ApplicationAccessible.cpp", + "ARIAGridAccessible.cpp", + "BaseAccessibles.cpp", + "DocAccessible.cpp", + "FormControlAccessible.cpp", + "HyperTextAccessible.cpp", + "ImageAccessible.cpp", + "LocalAccessible.cpp", + "OuterDocAccessible.cpp", + "RootAccessible.cpp", + "TableAccessible.cpp", + "TableCellAccessible.cpp", + "TextLeafAccessible.cpp", +] + +LOCAL_INCLUDES += [ + "/accessible/base", + "/accessible/html", + "/accessible/xpcom", + "/accessible/xul", + "/dom/base", + "/dom/xul", + "/layout/generic", + "/layout/xul", +] + +if CONFIG["OS_ARCH"] == "WINNT": + LOCAL_INCLUDES += [ + "/accessible/ipc/win", + ] +else: + LOCAL_INCLUDES += [ + "/accessible/ipc/other", + ] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + LOCAL_INCLUDES += [ + "/accessible/atk", + ] +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") |