diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /accessible/generic | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'accessible/generic')
30 files changed, 14334 insertions, 0 deletions
diff --git a/accessible/generic/ARIAGridAccessible-inl.h b/accessible/generic/ARIAGridAccessible-inl.h new file mode 100644 index 0000000000..bb2bc9705b --- /dev/null +++ b/accessible/generic/ARIAGridAccessible-inl.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef 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(Accessible* aRow) const +{ + Accessible* table = nsAccUtils::TableFor(aRow); + if (table) { + int32_t rowIdx = 0; + Accessible* 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..48de9bbf07 --- /dev/null +++ b/accessible/generic/ARIAGridAccessible.cpp @@ -0,0 +1,702 @@ +/* -*- 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 "Accessible-inl.h" +#include "AccIterator.h" +#include "nsAccUtils.h" +#include "Role.h" +#include "States.h" + +#include "nsIMutableArray.h" +#include "nsIPersistentProperties2.h" +#include "nsComponentManagerUtils.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// ARIAGridAccessible +//////////////////////////////////////////////////////////////////////////////// + + +//////////////////////////////////////////////////////////////////////////////// +// Constructor + +ARIAGridAccessible:: + ARIAGridAccessible(nsIContent* aContent, DocAccessible* aDoc) : + AccessibleWrap(aContent, aDoc) +{ +} + +NS_IMPL_ISUPPORTS_INHERITED0(ARIAGridAccessible, Accessible) + +//////////////////////////////////////////////////////////////////////////////// +// Table + +uint32_t +ARIAGridAccessible::ColCount() +{ + AccIterator rowIter(this, filters::GetRow); + Accessible* row = rowIter.Next(); + if (!row) + return 0; + + AccIterator cellIter(row, filters::GetCell); + Accessible* cell = nullptr; + + uint32_t colCount = 0; + while ((cell = cellIter.Next())) + colCount++; + + return colCount; +} + +uint32_t +ARIAGridAccessible::RowCount() +{ + uint32_t rowCount = 0; + AccIterator rowIter(this, filters::GetRow); + while (rowIter.Next()) + rowCount++; + + return rowCount; +} + +Accessible* +ARIAGridAccessible::CellAt(uint32_t aRowIndex, uint32_t aColumnIndex) +{ + Accessible* row = GetRowAt(aRowIndex); + if (!row) + return nullptr; + + return GetCellInRowAt(row, aColumnIndex); +} + +bool +ARIAGridAccessible::IsColSelected(uint32_t aColIdx) +{ + if (IsARIARole(nsGkAtoms::table)) + return false; + + AccIterator rowIter(this, filters::GetRow); + Accessible* row = rowIter.Next(); + if (!row) + return false; + + do { + if (!nsAccUtils::IsARIASelected(row)) { + Accessible* cell = GetCellInRowAt(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; + + Accessible* row = GetRowAt(aRowIdx); + if(!row) + return false; + + if (!nsAccUtils::IsARIASelected(row)) { + AccIterator cellIter(row, filters::GetCell); + Accessible* 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; + + Accessible* row = GetRowAt(aRowIdx); + if(!row) + return false; + + if (!nsAccUtils::IsARIASelected(row)) { + Accessible* cell = GetCellInRowAt(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); + Accessible* row = nullptr; + + while ((row = rowIter.Next())) { + if (nsAccUtils::IsARIASelected(row)) { + count += colCount; + continue; + } + + AccIterator cellIter(row, filters::GetCell); + Accessible* 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); + Accessible* 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); + Accessible* 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); + Accessible* row = nullptr; + + while ((row = rowIter.Next())) { + if (nsAccUtils::IsARIASelected(row)) { + count++; + continue; + } + + AccIterator cellIter(row, filters::GetCell); + Accessible* 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); + + Accessible* row = nullptr; + while ((row = rowIter.Next())) { + AccIterator cellIter(row, filters::GetCell); + Accessible* 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); + Accessible* 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); + Accessible* 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); + Accessible* 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); + Accessible* 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); + Accessible* row = nullptr; + for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) { + if (nsAccUtils::IsARIASelected(row)) { + aRows->AppendElement(rowIdx); + continue; + } + + AccIterator cellIter(row, filters::GetCell); + Accessible* 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); + + Accessible* 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); + + Accessible* 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. + Accessible* cell = GetCellInRowAt(row, aColIdx); + if (cell) + SetARIASelected(cell, true); + } +} + +void +ARIAGridAccessible::UnselectRow(uint32_t aRowIdx) +{ + if (IsARIARole(nsGkAtoms::table)) + return; + + Accessible* row = GetRowAt(aRowIdx); + if (row) + SetARIASelected(row, false); +} + +void +ARIAGridAccessible::UnselectCol(uint32_t aColIdx) +{ + if (IsARIARole(nsGkAtoms::table)) + return; + + AccIterator rowIter(this, filters::GetRow); + + Accessible* row = nullptr; + while ((row = rowIter.Next())) { + Accessible* cell = GetCellInRowAt(row, aColIdx); + if (cell) + SetARIASelected(cell, false); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Protected + +Accessible* +ARIAGridAccessible::GetRowAt(int32_t aRow) +{ + int32_t rowIdx = aRow; + + AccIterator rowIter(this, filters::GetRow); + + Accessible* row = rowIter.Next(); + while (rowIdx != 0 && (row = rowIter.Next())) + rowIdx--; + + return row; +} + +Accessible* +ARIAGridAccessible::GetCellInRowAt(Accessible* aRow, int32_t aColumn) +{ + int32_t colIdx = aColumn; + + AccIterator cellIter(aRow, filters::GetCell); + Accessible* cell = cellIter.Next(); + while (colIdx != 0 && (cell = cellIter.Next())) + colIdx--; + + return cell; +} + +nsresult +ARIAGridAccessible::SetARIASelected(Accessible* 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 (aIsSelected) + rv = content->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_selected, + NS_LITERAL_STRING("true"), aNotify); + else + rv = content->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_selected, + NS_LITERAL_STRING("false"), 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); + Accessible* 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) { + Accessible* row = aAccessible->Parent(); + + if (row && row->Role() == roles::ROW && + nsAccUtils::IsARIASelected(row)) { + rv = SetARIASelected(row, false, false); + NS_ENSURE_SUCCESS(rv, rv); + + AccIterator cellIter(row, filters::GetCell); + Accessible* 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) : + AccessibleWrap(aContent, aDoc) +{ + mGenericTypes |= eTableRow; +} + +NS_IMPL_ISUPPORTS_INHERITED0(ARIARowAccessible, Accessible) + +GroupPos +ARIARowAccessible::GroupPosition() +{ + int32_t count = 0, index = 0; + Accessible* table = nsAccUtils::TableFor(this); + if (table && nsCoreUtils::GetUIntAttr(table->GetContent(), + nsGkAtoms::aria_rowcount, &count) && + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_rowindex, &index)) { + return GroupPos(0, index, count); + } + + return AccessibleWrap::GroupPosition(); +} + + +//////////////////////////////////////////////////////////////////////////////// +// ARIAGridCellAccessible +//////////////////////////////////////////////////////////////////////////////// + + +//////////////////////////////////////////////////////////////////////////////// +// Constructor + +ARIAGridCellAccessible:: + ARIAGridCellAccessible(nsIContent* aContent, DocAccessible* aDoc) : + HyperTextAccessibleWrap(aContent, aDoc) +{ + mGenericTypes |= eTableCell; +} + +NS_IMPL_ISUPPORTS_INHERITED0(ARIAGridCellAccessible, HyperTextAccessible) + +//////////////////////////////////////////////////////////////////////////////// +// TableCell + +TableAccessible* +ARIAGridCellAccessible::Table() const +{ + Accessible* table = nsAccUtils::TableFor(Row()); + return table ? table->AsTable() : nullptr; +} + +uint32_t +ARIAGridCellAccessible::ColIdx() const +{ + Accessible* row = Row(); + if (!row) + return 0; + + int32_t indexInRow = IndexInParent(); + uint32_t colIdx = 0; + for (int32_t idx = 0; idx < indexInRow; idx++) { + Accessible* cell = row->GetChildAt(idx); + roles::Role role = cell->Role(); + if (role == roles::CELL || role == roles::GRID_CELL || + role == roles::ROWHEADER || role == roles::COLUMNHEADER) + colIdx++; + } + + return colIdx; +} + +uint32_t +ARIAGridCellAccessible::RowIdx() const +{ + return RowIndexFor(Row()); +} + +bool +ARIAGridCellAccessible::Selected() +{ + Accessible* row = Row(); + if (!row) + return false; + + return nsAccUtils::IsARIASelected(row) || nsAccUtils::IsARIASelected(this); +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible + +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. + Accessible* row = Parent(); + if (!row || row->Role() != roles::ROW) + return; + + nsIContent *rowContent = row->GetContent(); + if (nsAccUtils::HasDefinedARIAToken(rowContent, + nsGkAtoms::aria_selected) && + !rowContent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::aria_selected, + nsGkAtoms::_false, eCaseMatters)) + *aState |= states::SELECTABLE | states::SELECTED; +} + +already_AddRefed<nsIPersistentProperties> +ARIAGridCellAccessible::NativeAttributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = + HyperTextAccessibleWrap::NativeAttributes(); + + // Expose "table-cell-index" attribute. + Accessible* thisRow = Row(); + if (!thisRow) + return attributes.forget(); + + int32_t colIdx = 0, colCount = 0; + uint32_t childCount = thisRow->ChildCount(); + for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { + Accessible* child = thisRow->GetChildAt(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++; + } + + int32_t rowIdx = RowIndexFor(thisRow); + + nsAutoString stringIdx; + stringIdx.AppendInt(rowIdx * colCount + colIdx); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::tableCellIndex, stringIdx); + +#ifdef DEBUG + nsAutoString unused; + attributes->SetStringProperty(NS_LITERAL_CSTRING("cppclass"), + NS_LITERAL_STRING("ARIAGridCellAccessible"), + unused); +#endif + + return attributes.forget(); +} + +GroupPos +ARIAGridCellAccessible::GroupPosition() +{ + int32_t count = 0, index = 0; + TableAccessible* table = Table(); + if (table && nsCoreUtils::GetUIntAttr(table->AsAccessible()->GetContent(), + nsGkAtoms::aria_colcount, &count) && + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_colindex, &index)) { + return GroupPos(0, index, count); + } + + return GroupPos(); +} diff --git a/accessible/generic/ARIAGridAccessible.h b/accessible/generic/ARIAGridAccessible.h new file mode 100644 index 0000000000..c9a36cc6ef --- /dev/null +++ b/accessible/generic/ARIAGridAccessible.h @@ -0,0 +1,138 @@ +/* -*- 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 AccessibleWrap, + public TableAccessible +{ +public: + ARIAGridAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_DECL_ISUPPORTS_INHERITED + + // Accessible + virtual TableAccessible* AsTable() override { return this; } + + // TableAccessible + virtual uint32_t ColCount() override; + virtual uint32_t RowCount() override; + virtual Accessible* 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 Accessible* AsAccessible() override { return this; } + +protected: + virtual ~ARIAGridAccessible() {} + + /** + * Return row accessible at the given row index. + */ + Accessible* GetRowAt(int32_t aRow); + + /** + * Return cell accessible at the given column index in the row. + */ + Accessible* GetCellInRowAt(Accessible* aRow, int32_t aColumn); + + /** + * 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(Accessible* aAccessible, bool aIsSelected, + bool aNotify = true); +}; + + +/** + * Accessible for ARIA row. + */ +class ARIARowAccessible : public AccessibleWrap +{ +public: + ARIARowAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_DECL_ISUPPORTS_INHERITED + + // Accessible + virtual mozilla::a11y::GroupPos GroupPosition() override; + +protected: + virtual ~ARIARowAccessible() {} +}; + + +/** + * Accessible for ARIA gridcell and rowheader/columnheader. + */ +class ARIAGridCellAccessible : public HyperTextAccessibleWrap, + public TableCellAccessible +{ +public: + ARIAGridCellAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_DECL_ISUPPORTS_INHERITED + + // Accessible + virtual TableCellAccessible* AsTableCell() override { return this; } + virtual void ApplyARIAState(uint64_t* aState) const override; + virtual already_AddRefed<nsIPersistentProperties> NativeAttributes() override; + virtual mozilla::a11y::GroupPos GroupPosition() override; + +protected: + virtual ~ARIAGridCellAccessible() {} + + /** + * Return a containing row. + */ + Accessible* Row() const + { + Accessible* row = Parent(); + return row && row->IsTableRow() ? row : nullptr; + } + + /** + * Return index of the given row. + */ + int32_t RowIndexFor(Accessible* 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/Accessible-inl.h b/accessible/generic/Accessible-inl.h new file mode 100644 index 0000000000..f80056479e --- /dev/null +++ b/accessible/generic/Accessible-inl.h @@ -0,0 +1,135 @@ +/* -*- 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" + +#ifdef A11Y_LOG +#include "Logging.h" +#endif + +namespace mozilla { +namespace a11y { + +inline mozilla::a11y::role +Accessible::Role() +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole) + return ARIATransformRole(NativeRole()); + + return ARIATransformRole(roleMapEntry->role); +} + +inline bool +Accessible::HasARIARole() const +{ + return mRoleMapEntryIndex != aria::NO_ROLE_MAP_ENTRY_INDEX; +} + +inline bool +Accessible::IsARIARole(nsIAtom* aARIARole) const +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return roleMapEntry && roleMapEntry->Is(aARIARole); +} + +inline bool +Accessible::HasStrongARIARole() const +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return roleMapEntry && roleMapEntry->roleRule == kUseMapRole; +} + +inline const nsRoleMapEntry* +Accessible::ARIARoleMap() const +{ + return aria::GetRoleMapFromIndex(mRoleMapEntryIndex); +} + +inline mozilla::a11y::role +Accessible::ARIARole() +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole) + return mozilla::a11y::roles::NOTHING; + + return ARIATransformRole(roleMapEntry->role); +} + +inline void +Accessible::SetRoleMapEntry(const nsRoleMapEntry* aRoleMapEntry) +{ + mRoleMapEntryIndex = aria::GetIndexFromRoleMap(aRoleMapEntry); +} + +inline bool +Accessible::IsSearchbox() const +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return (roleMapEntry && roleMapEntry->Is(nsGkAtoms::searchbox)) || + (mContent->IsHTMLElement(nsGkAtoms::input) && + mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::textInputType, eCaseMatters)); +} + +inline bool +Accessible::HasGenericType(AccGenericType aType) const +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return (mGenericTypes & aType) || + (roleMapEntry && roleMapEntry->IsOfType(aType)); +} + +inline bool +Accessible::HasNumericValue() const +{ + if (mStateFlags & eHasNumericValue) + return true; + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return roleMapEntry && roleMapEntry->valueRule != eNoValue; +} + +inline void +Accessible::ScrollTo(uint32_t aHow) const +{ + if (mContent) + nsCoreUtils::ScrollTo(mDoc->PresShell(), mContent, aHow); +} + +inline bool +Accessible::InsertAfter(Accessible* aNewChild, Accessible* aRefChild) +{ + MOZ_ASSERT(aNewChild, "No new child to insert"); + + if (aRefChild && aRefChild->Parent() != this) { +#ifdef A11Y_LOG + logging::TreeInfo("broken accessible tree", 0, + "parent", this, "prev sibling parent", + aRefChild->Parent(), "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/Accessible.cpp b/accessible/generic/Accessible.cpp new file mode 100644 index 0000000000..7ff2f82836 --- /dev/null +++ b/accessible/generic/Accessible.cpp @@ -0,0 +1,2856 @@ +/* -*- 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 "Accessible-inl.h" + +#include "nsIXBLAccessible.h" + +#include "EmbeddedObjCollector.h" +#include "AccGroupInfo.h" +#include "AccIterator.h" +#include "nsAccUtils.h" +#include "nsAccessibilityService.h" +#include "ApplicationAccessible.h" +#include "NotificationController.h" +#include "nsEventShell.h" +#include "nsTextEquivUtils.h" +#include "DocAccessibleChild.h" +#include "EventTree.h" +#include "Relation.h" +#include "Role.h" +#include "RootAccessible.h" +#include "States.h" +#include "StyleInfo.h" +#include "TableAccessible.h" +#include "TableCellAccessible.h" +#include "TreeWalker.h" + +#include "nsIDOMElement.h" +#include "nsIDOMNodeFilter.h" +#include "nsIDOMHTMLElement.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMTreeWalker.h" +#include "nsIDOMXULButtonElement.h" +#include "nsIDOMXULDocument.h" +#include "nsIDOMXULElement.h" +#include "nsIDOMXULLabelElement.h" +#include "nsIDOMXULSelectCntrlEl.h" +#include "nsIDOMXULSelectCntrlItemEl.h" +#include "nsPIDOMWindow.h" + +#include "nsIDocument.h" +#include "nsIContent.h" +#include "nsIForm.h" +#include "nsIFormControl.h" + +#include "nsDeckFrame.h" +#include "nsLayoutUtils.h" +#include "nsIPresShell.h" +#include "nsIStringBundle.h" +#include "nsPresContext.h" +#include "nsIFrame.h" +#include "nsView.h" +#include "nsIDocShellTreeItem.h" +#include "nsIScrollableFrame.h" +#include "nsFocusManager.h" + +#include "nsXPIDLString.h" +#include "nsUnicharUtils.h" +#include "nsReadableUtils.h" +#include "prdtoa.h" +#include "nsIAtom.h" +#include "nsIURI.h" +#include "nsArrayUtils.h" +#include "nsIMutableArray.h" +#include "nsIObserverService.h" +#include "nsIServiceManager.h" +#include "nsWhitespaceTokenizer.h" +#include "nsAttrName.h" + +#ifdef DEBUG +#include "nsIDOMCharacterData.h" +#endif + +#include "mozilla/Assertions.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventStates.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Unused.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/CanvasRenderingContext2D.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "mozilla/dom/TreeWalker.h" + +using namespace mozilla; +using namespace mozilla::a11y; + + +//////////////////////////////////////////////////////////////////////////////// +// Accessible: nsISupports and cycle collection + +NS_IMPL_CYCLE_COLLECTION_CLASS(Accessible) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Accessible) + tmp->Shutdown(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Accessible) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent, mDoc) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Accessible) + if (aIID.Equals(NS_GET_IID(Accessible))) + foundInterface = this; + else + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, Accessible) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Accessible) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_DESTROY(Accessible, LastRelease()) + +Accessible::Accessible(nsIContent* aContent, DocAccessible* aDoc) : + mContent(aContent), mDoc(aDoc), + mParent(nullptr), mIndexInParent(-1), + mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX), + mStateFlags(0), mContextFlags(0), mType(0), mGenericTypes(0), + mReorderEventTarget(false), mShowEventTarget(false), mHideEventTarget(false) +{ + mBits.groupInfo = nullptr; + mInt.mIndexOfEmbeddedChild = -1; +} + +Accessible::~Accessible() +{ + NS_ASSERTION(!mDoc, "LastRelease was never called!?!"); +} + +ENameValueFlag +Accessible::Name(nsString& aName) +{ + aName.Truncate(); + + if (!HasOwnContent()) + return eNameOK; + + ARIAName(aName); + if (!aName.IsEmpty()) + return eNameOK; + + nsCOMPtr<nsIXBLAccessible> xblAccessible(do_QueryInterface(mContent)); + if (xblAccessible) { + xblAccessible->GetAccessibleName(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->GetAttr(kNameSpaceID_None, nsGkAtoms::title, aName)) { + aName.CompressWhitespace(); + return eNameFromTooltip; + } + } else if (mContent->IsXULElement()) { + if (mContent->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; + } + } + } + + if (nameFlag != eNoNameOnPurpose) + aName.SetIsVoid(true); + + return nameFlag; +} + +void +Accessible::Description(nsString& aDescription) +{ + // There are 4 conditions that make an accessible have no accDescription: + // 1. it's a text node; or + // 2. It has no DHTML describedby property + // 3. it doesn't have an accName; or + // 4. its title attribute already equals to its accName nsAutoString name; + + if (!HasOwnContent() || mContent->IsNodeOfType(nsINode::eTEXT)) + return; + + nsTextEquivUtils:: + GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, + aDescription); + + if (aDescription.IsEmpty()) { + NativeDescription(aDescription); + + if (aDescription.IsEmpty()) { + // Keep the Name() method logic. + if (mContent->IsHTMLElement()) { + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::title, aDescription); + } else if (mContent->IsXULElement()) { + mContent->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 +Accessible::AccessKey() const +{ + if (!HasOwnContent()) + return KeyBinding(); + + uint32_t key = nsCoreUtils::GetAccessKeyFor(mContent); + if (!key && mContent->IsElement()) { + Accessible* 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(); + + } else if (mContent->IsXULElement()) { + 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 (Preferences::GetInt("ui.key.generalAccessKey", -1)) { + case -1: + break; + case nsIDOMKeyEvent::DOM_VK_SHIFT: + return KeyBinding(key, KeyBinding::kShift); + case nsIDOMKeyEvent::DOM_VK_CONTROL: + return KeyBinding(key, KeyBinding::kControl); + case nsIDOMKeyEvent::DOM_VK_ALT: + return KeyBinding(key, KeyBinding::kAlt); + case nsIDOMKeyEvent::DOM_VK_META: + return KeyBinding(key, KeyBinding::kMeta); + default: + return KeyBinding(); + } + + // Determine the access modifier used in this context. + nsIDocument* document = mContent->GetUncomposedDoc(); + 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: + rv = Preferences::GetInt("ui.key.chromeAccess", &modifierMask); + break; + case nsIDocShellTreeItem::typeContent: + rv = Preferences::GetInt("ui.key.contentAccess", &modifierMask); + break; + } + + return NS_SUCCEEDED(rv) ? KeyBinding(key, modifierMask) : KeyBinding(); +} + +KeyBinding +Accessible::KeyboardShortcut() const +{ + return KeyBinding(); +} + +void +Accessible::TranslateString(const nsString& aKey, nsAString& aStringOut) +{ + nsCOMPtr<nsIStringBundleService> stringBundleService = + services::GetStringBundleService(); + if (!stringBundleService) + return; + + nsCOMPtr<nsIStringBundle> stringBundle; + stringBundleService->CreateBundle( + "chrome://global-platform/locale/accessible.properties", + getter_AddRefs(stringBundle)); + if (!stringBundle) + return; + + nsXPIDLString xsValue; + nsresult rv = stringBundle->GetStringFromName(aKey.get(), getter_Copies(xsValue)); + if (NS_SUCCEEDED(rv)) + aStringOut.Assign(xsValue); +} + +uint64_t +Accessible::VisibilityState() +{ + nsIFrame* frame = GetFrame(); + if (!frame) + return states::INVISIBLE; + + // Walk the parent frame chain to see if there's invisible parent or the frame + // is in background tab. + if (!frame->StyleVisibility()->IsVisible()) + return states::INVISIBLE; + + nsIFrame* curFrame = frame; + do { + nsView* view = curFrame->GetView(); + if (view && view->GetVisibility() == nsViewVisibility_kHide) + return states::INVISIBLE; + + if (nsLayoutUtils::IsPopup(curFrame)) + return 0; + + // Offscreen state for background tab content and invisible for not selected + // deck panel. + nsIFrame* parentFrame = curFrame->GetParent(); + nsDeckFrame* deckFrame = do_QueryFrame(parentFrame); + if (deckFrame && deckFrame->GetSelectedBox() != curFrame) { + if (deckFrame->GetContent()->IsXULElement(nsGkAtoms::tabpanels)) + return states::OFFSCREEN; + + NS_NOTREACHED("Children of not selected deck panel are not accessible."); + return states::INVISIBLE; + } + + // 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); + if (scrollableFrame) { + nsRect scrollPortRect = scrollableFrame->GetScrollPortRect(); + nsRect frameRect = nsLayoutUtils::TransformFrameRectToAncestor( + frame, frame->GetRectRelativeToSelf(), parentFrame); + if (!scrollPortRect.Contains(frameRect)) { + const nscoord kMinPixels = nsPresContext::CSSPixelsToAppUnits(12); + scrollPortRect.Deflate(kMinPixels, kMinPixels); + if (!scrollPortRect.Intersects(frameRect)) + return states::OFFSCREEN; + } + } + + if (!parentFrame) { + parentFrame = nsLayoutUtils::GetCrossDocParentFrame(curFrame); + if (parentFrame && !parentFrame->StyleVisibility()->IsVisible()) + return states::INVISIBLE; + } + + 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->GetType() == nsGkAtoms::textFrame && + !(frame->GetStateBits() & NS_FRAME_OUT_OF_FLOW) && + frame->GetRect().IsEmpty()) { + nsIFrame::RenderedText text = frame->GetRenderedText(0, + UINT32_MAX, nsIFrame::TextOffsetType::OFFSETS_IN_CONTENT_TEXT, + nsIFrame::TrailingWhitespace::DONT_TRIM_TRAILING_WHITESPACE); + if (text.mString.IsEmpty()) { + return states::INVISIBLE; + } + } + + return 0; +} + +uint64_t +Accessible::NativeState() +{ + uint64_t state = 0; + + if (!IsInDocument()) + state |= states::STALE; + + if (HasOwnContent() && mContent->IsElement()) { + EventStates elementState = mContent->AsElement()->State(); + + if (elementState.HasState(NS_EVENT_STATE_INVALID)) + state |= states::INVALID; + + if (elementState.HasState(NS_EVENT_STATE_REQUIRED)) + state |= states::REQUIRED; + + state |= NativeInteractiveState(); + if (FocusMgr()->IsFocused(this)) + state |= states::FOCUSED; + } + + // 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->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 +Accessible::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 +Accessible::NativeLinkState() const +{ + return 0; +} + +bool +Accessible::NativelyUnavailable() const +{ + if (mContent->IsHTMLElement()) + return mContent->AsElement()->State().HasState(NS_EVENT_STATE_DISABLED); + + return mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters); +} + +Accessible* +Accessible::FocusedChild() +{ + Accessible* focus = FocusMgr()->FocusedAccessible(); + if (focus && (focus == this || focus->Parent() == this)) + return focus; + + return nullptr; +} + +Accessible* +Accessible::ChildAtPoint(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. + Accessible* fallbackAnswer = nullptr; + nsIntRect rect = Bounds(); + if (aX >= rect.x && aX < rect.x + rect.width && + aY >= rect.y && aY < rect.y + rect.height) + 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(); + + WidgetMouseEvent dummyEvent(true, eMouseMove, rootWidget, + WidgetMouseEvent::eSynthesized); + dummyEvent.mRefPoint = LayoutDeviceIntPoint(aX - rootRect.x, aY - rootRect.y); + + nsIFrame* popupFrame = nsLayoutUtils:: + GetPopupFrameForEventCoordinates(accDocument->PresContext()->GetRootPresContext(), + &dummyEvent); + 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()); + Accessible* popupAcc = + popupDoc->GetAccessibleOrContainer(popupFrame->GetContent()); + Accessible* popupChild = this; + while (popupChild && !popupChild->IsDoc() && popupChild != popupAcc) + popupChild = popupChild->Parent(); + + 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(startFrame, 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; + + Accessible* 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. + Accessible* child = accessible; + while (child != this) { + Accessible* parent = child->Parent(); + 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 == eDirectChild) + 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(); + for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { + Accessible* child = accessible->GetChildAt(childIdx); + + nsIntRect childRect = child->Bounds(); + if (aX >= childRect.x && aX < childRect.x + childRect.width && + aY >= childRect.y && aY < childRect.y + childRect.height && + (child->State() & states::INVISIBLE) == 0) { + + if (aWhichChild == eDeepestChild) + return child->ChildAtPoint(aX, aY, eDeepestChild); + + return child; + } + } + + return accessible; +} + +nsRect +Accessible::RelativeBounds(nsIFrame** aBoundingFrame) const +{ + nsIFrame* frame = GetFrame(); + if (frame && mContent) { + bool* hasHitRegionRect = static_cast<bool*>(mContent->GetProperty(nsGkAtoms::hitregion)); + + if (hasHitRegionRect && mContent->IsElement()) { + // This is for canvas fallback content + // Find a canvas frame the found hit region is relative to. + nsIFrame* canvasFrame = frame->GetParent(); + if (canvasFrame) { + canvasFrame = nsLayoutUtils::GetClosestFrameOfType(canvasFrame, nsGkAtoms::HTMLCanvasFrame); + } + + // make the canvas the bounding frame + if (canvasFrame) { + *aBoundingFrame = canvasFrame; + dom::HTMLCanvasElement *canvas = + dom::HTMLCanvasElement::FromContent(canvasFrame->GetContent()); + + // get the bounding rect of the hit region + nsRect bounds; + if (canvas && canvas->CountContexts() && + canvas->GetContextAtIndex(0)->GetHitRegionRect(mContent->AsElement(), bounds)) { + return bounds; + } + } + } + + *aBoundingFrame = nsLayoutUtils::GetContainingBlockForClientRect(frame); + return nsLayoutUtils:: + GetAllInFlowRectsUnion(frame, *aBoundingFrame, + nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS); + } + + return nsRect(); +} + +nsIntRect +Accessible::Bounds() const +{ + nsIFrame* boundingFrame = nullptr; + nsRect unionRectTwips = RelativeBounds(&boundingFrame); + if (!boundingFrame) + return nsIntRect(); + + nsIntRect screenRect; + nsPresContext* presContext = mDoc->PresContext(); + screenRect.x = presContext->AppUnitsToDevPixels(unionRectTwips.x); + screenRect.y = presContext->AppUnitsToDevPixels(unionRectTwips.y); + screenRect.width = presContext->AppUnitsToDevPixels(unionRectTwips.width); + screenRect.height = presContext->AppUnitsToDevPixels(unionRectTwips.height); + + // We have the union of the rectangle, now we need to put it in absolute + // screen coords. + nsIntRect orgRectPixels = boundingFrame->GetScreenRectInAppUnits(). + ToNearestPixels(presContext->AppUnitsPerDevPixel()); + screenRect.x += orgRectPixels.x; + screenRect.y += orgRectPixels.y; + + return screenRect; +} + +void +Accessible::SetSelected(bool aSelect) +{ + if (!HasOwnContent()) + return; + + Accessible* select = nsAccUtils::GetSelectableContainer(this, State()); + if (select) { + if (select->State() & states::MULTISELECTABLE) { + if (ARIARoleMap()) { + if (aSelect) { + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_selected, + NS_LITERAL_STRING("true"), true); + } else { + mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::aria_selected, true); + } + } + return; + } + + if (aSelect) + TakeFocus(); + } +} + +void +Accessible::TakeSelection() +{ + Accessible* select = nsAccUtils::GetSelectableContainer(this, State()); + if (select) { + if (select->State() & states::MULTISELECTABLE) + select->UnselectAll(); + SetSelected(true); + } +} + +void +Accessible::TakeFocus() +{ + 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()) { + Accessible* widget = ContainerWidget(); + if (widget && widget->AreItemsOperable()) { + nsIContent* widgetElm = widget->GetContent(); + nsIFrame* widgetFrame = widgetElm->GetPrimaryFrame(); + if (widgetFrame && widgetFrame->IsFocusable()) { + focusContent = widgetElm; + widget->SetCurrentItem(this); + } + } + } + + nsCOMPtr<nsIDOMElement> element(do_QueryInterface(focusContent)); + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm) + fm->SetFocus(element, 0); +} + +void +Accessible::XULElmName(DocAccessible* aDocument, + nsIContent* aElm, nsString& aName) +{ + /** + * 3 main cases for XUL Controls to be labeled + * 1 - control contains label="foo" + * 2 - control has, as a child, a label element + * - label has either value="foo" or children + * 3 - non-child label contains control="controlID" + * - label has either value="foo" or children + * Once a label is found, the search is discontinued, so a control + * that has a label child as well as having a label external to + * the control that uses the control="controlID" syntax will use + * the child label for its Name. + */ + + // CASE #1 (via label attribute) -- great majority of the cases + nsCOMPtr<nsIDOMXULLabeledControlElement> labeledEl = do_QueryInterface(aElm); + if (labeledEl) { + labeledEl->GetLabel(aName); + } else { + nsCOMPtr<nsIDOMXULSelectControlItemElement> itemEl = do_QueryInterface(aElm); + if (itemEl) { + itemEl->GetLabel(aName); + } else { + nsCOMPtr<nsIDOMXULSelectControlElement> select = do_QueryInterface(aElm); + // Use label if this is not a select control element which + // uses label attribute to indicate which option is selected + if (!select) { + nsCOMPtr<nsIDOMXULElement> xulEl(do_QueryInterface(aElm)); + if (xulEl) + xulEl->GetAttribute(NS_LITERAL_STRING("label"), aName); + } + } + } + + // CASES #2 and #3 ------ label as a child or <label control="id" ... > </label> + if (aName.IsEmpty()) { + Accessible* labelAcc = nullptr; + XULLabelIterator iter(aDocument, aElm); + while ((labelAcc = iter.Next())) { + nsCOMPtr<nsIDOMXULLabelElement> xulLabel = + do_QueryInterface(labelAcc->GetContent()); + // Check if label's value attribute is used + if (xulLabel && NS_SUCCEEDED(xulLabel->GetValue(aName)) && aName.IsEmpty()) { + // If no value attribute, a non-empty label must contain + // children that define its text -- possibly using HTML + nsTextEquivUtils:: + AppendTextEquivFromContent(labelAcc, labelAcc->GetContent(), &aName); + } + } + } + + aName.CompressWhitespace(); + if (!aName.IsEmpty()) + return; + + // Can get text from title of <toolbaritem> if we're a child of a <toolbaritem> + nsIContent *bindingParent = aElm->GetBindingParent(); + nsIContent* parent = + bindingParent? bindingParent->GetParent() : aElm->GetParent(); + nsAutoString ancestorTitle; + while (parent) { + if (parent->IsXULElement(nsGkAtoms::toolbaritem) && + parent->GetAttr(kNameSpaceID_None, nsGkAtoms::title, ancestorTitle)) { + // Before returning this, check if the element itself has a tooltip: + if (aElm->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext, aName)) { + aName.CompressWhitespace(); + return; + } + + aName.Assign(ancestorTitle); + aName.CompressWhitespace(); + return; + } + parent = parent->GetParent(); + } +} + +nsresult +Accessible::HandleAccEvent(AccEvent* aEvent) +{ + NS_ENSURE_ARG_POINTER(aEvent); + + if (IPCAccessibilityActive() && Document()) { + DocAccessibleChild* ipcDoc = mDoc->IPCDoc(); + MOZ_ASSERT(ipcDoc); + if (ipcDoc) { + uint64_t id = aEvent->GetAccessible()->IsDoc() ? 0 : + reinterpret_cast<uintptr_t>(aEvent->GetAccessible()); + + 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_REORDER: + // 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()); + break; + } + case nsIAccessibleEvent::EVENT_TEXT_INSERTED: + case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { + AccTextChangeEvent* event = downcast_accEvent(aEvent); + ipcDoc->SendTextChangeEvent(id, event->ModifiedText(), + event->GetStartOffset(), + event->GetLength(), + event->IsTextInserted(), + event->IsFromUserInput()); + break; + } + case nsIAccessibleEvent::EVENT_SELECTION: + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: { + AccSelChangeEvent* selEvent = downcast_accEvent(aEvent); + uint64_t widgetID = selEvent->Widget()->IsDoc() ? 0 : + reinterpret_cast<uintptr_t>(selEvent->Widget()); + ipcDoc->SendSelectionEvent(id, widgetID, aEvent->GetEventType()); + break; + } + default: + ipcDoc->SendEvent(id, aEvent->GetEventType()); + } + } + } + + if (nsCoreUtils::AccEventObserversExist()) { + nsCoreUtils::DispatchAccEvent(MakeXPCEvent(aEvent)); + } + + return NS_OK; +} + +already_AddRefed<nsIPersistentProperties> +Accessible::Attributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = NativeAttributes(); + if (!HasOwnContent() || !mContent->IsElement()) + return attributes.forget(); + + // 'xml-roles' attribute for landmark. + nsIAtom* landmark = LandmarkRole(); + if (landmark) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::xmlroles, landmark); + + } else { + // 'xml-roles' attribute coming from ARIA. + nsAutoString xmlRoles; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::role, xmlRoles)) + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::xmlroles, xmlRoles); + } + + // Expose object attributes from ARIA attributes. + nsAutoString unused; + aria::AttrIterator attribIter(mContent); + nsAutoString name, value; + while(attribIter.Next(name, value)) + attributes->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused); + + if (IsARIAHidden()) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::hidden, + NS_LITERAL_STRING("true")); + } + + // 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)) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::textInputType, + NS_LITERAL_STRING("search")); + } + + nsAutoString live; + nsAccUtils::GetAccAttr(attributes, nsGkAtoms::live, live); + if (live.IsEmpty()) { + if (nsAccUtils::GetLiveAttrValue(roleMapEntry->liveAttRule, live)) + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::live, live); + } + } + + return attributes.forget(); +} + +already_AddRefed<nsIPersistentProperties> +Accessible::NativeAttributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = + do_CreateInstance(NS_PERSISTENTPROPERTIES_CONTRACTID); + + nsAutoString unused; + + // 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()) { + nsAutoString valuetext; + Value(valuetext); + attributes->SetStringProperty(NS_LITERAL_CSTRING("valuetext"), valuetext, + unused); + } + + // Expose checkable object attribute if the accessible has checkable state + if (State() & states::CHECKABLE) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::checkable, + NS_LITERAL_STRING("true")); + } + + // Expose 'explicit-name' attribute. + nsAutoString name; + if (Name(name) != eNameFromSubtree && !name.IsVoid()) { + attributes->SetStringProperty(NS_LITERAL_CSTRING("explicit-name"), + NS_LITERAL_STRING("true"), unused); + } + + // Group attributes (level/setsize/posinset) + GroupPos groupPos = GroupPosition(); + nsAccUtils::SetAccGroupAttrs(attributes, groupPos.level, + groupPos.setSize, groupPos.posInSet); + + // 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. However, nodes in outer + // documents override nodes in inner documents: outer doc author may want to + // override properties on a widget they used in an iframe. + nsIContent* startContent = mContent; + while (startContent) { + nsIDocument* doc = startContent->GetComposedDoc(); + if (!doc) + break; + + nsAccUtils::SetLiveContainerAttributes(attributes, startContent, + doc->GetRootElement()); + + // Allow ARIA live region markup from outer documents to override + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = doc->GetDocShell(); + if (!docShellTreeItem) + break; + + nsCOMPtr<nsIDocShellTreeItem> sameTypeParent; + docShellTreeItem->GetSameTypeParent(getter_AddRefs(sameTypeParent)); + if (!sameTypeParent || sameTypeParent == docShellTreeItem) + break; + + nsIDocument* parentDoc = doc->GetParentDocument(); + if (!parentDoc) + break; + + startContent = parentDoc->FindContentForSubDocument(doc); + } + + if (!mContent->IsElement()) + return attributes.forget(); + + nsAutoString id; + if (nsCoreUtils::GetID(mContent, id)) + attributes->SetStringProperty(NS_LITERAL_CSTRING("id"), id, unused); + + // Expose class because it may have useful microformat information. + nsAutoString _class; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::_class, _class)) + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::_class, _class); + + // Expose tag. + nsAutoString tagName; + mContent->NodeInfo()->GetName(tagName); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::tag, tagName); + + // Expose draggable object attribute. + nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(mContent); + if (htmlElement) { + bool draggable = false; + htmlElement->GetDraggable(&draggable); + if (draggable) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::draggable, + NS_LITERAL_STRING("true")); + } + } + + // Don't calculate CSS-based object attributes when no frame (i.e. + // the accessible is unattached from the tree). + if (!mContent->GetPrimaryFrame()) + return attributes.forget(); + + // CSS style based object attributes. + nsAutoString value; + StyleInfo styleInfo(mContent->AsElement(), mDoc->PresShell()); + + // Expose 'display' attribute. + styleInfo.Display(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::display, value); + + // Expose 'text-align' attribute. + styleInfo.TextAlign(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::textAlign, value); + + // Expose 'text-indent' attribute. + styleInfo.TextIndent(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::textIndent, value); + + // Expose 'margin-left' attribute. + styleInfo.MarginLeft(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::marginLeft, value); + + // Expose 'margin-right' attribute. + styleInfo.MarginRight(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::marginRight, value); + + // Expose 'margin-top' attribute. + styleInfo.MarginTop(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::marginTop, value); + + // Expose 'margin-bottom' attribute. + styleInfo.MarginBottom(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::marginBottom, value); + + return attributes.forget(); +} + +GroupPos +Accessible::GroupPosition() +{ + GroupPos groupPos; + if (!HasOwnContent()) + return groupPos; + + // Get group position from ARIA attributes. + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_level, &groupPos.level); + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_setsize, &groupPos.setSize); + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_posinset, &groupPos.posInSet); + + // If ARIA is missed and the accessible is visible then calculate group + // position from hierarchy. + if (State() & states::INVISIBLE) + return groupPos; + + // Calculate group level if ARIA is missed. + if (groupPos.level == 0) { + int32_t level = GetLevelInternal(); + if (level != 0) + groupPos.level = level; + } + + // Calculate position in group and group size if ARIA is missed. + if (groupPos.posInSet == 0 || groupPos.setSize == 0) { + int32_t posInSet = 0, setSize = 0; + GetPositionAndSizeInternal(&posInSet, &setSize); + if (posInSet != 0 && setSize != 0) { + if (groupPos.posInSet == 0) + groupPos.posInSet = posInSet; + + if (groupPos.setSize == 0) + groupPos.setSize = setSize; + } + } + + return groupPos; +} + +uint64_t +Accessible::State() +{ + if (IsDefunct()) + return states::DEFUNCT; + + uint64_t state = NativeState(); + // Apply ARIA states to be sure accessible states will be overridden. + ApplyARIAState(&state); + + // If this is an ARIA item of the selectable widget and if it's focused and + // not marked unselected explicitly (i.e. aria-selected="false") then expose + // it as selected to make ARIA widget authors life easier. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && !(state & states::SELECTED) && + !mContent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::aria_selected, + nsGkAtoms::_false, eCaseMatters)) { + // Special case for tabs: focused tab or focus inside related tab panel + // implies selected state. + if (roleMapEntry->role == roles::PAGETAB) { + if (state & states::FOCUSED) { + state |= states::SELECTED; + } else { + // If focus is in a child of the tab panel surely the tab is selected! + Relation rel = RelationByType(RelationType::LABEL_FOR); + Accessible* relTarget = nullptr; + while ((relTarget = rel.Next())) { + if (relTarget->Role() == roles::PROPERTYPAGE && + FocusMgr()->IsFocusWithin(relTarget)) + state |= states::SELECTED; + } + } + } else if (state & states::FOCUSED) { + Accessible* container = nsAccUtils::GetSelectableContainer(this, state); + if (container && + !nsAccUtils::HasDefinedARIAToken(container->GetContent(), + nsGkAtoms::aria_multiselectable)) { + state |= states::SELECTED; + } + } + } + + 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. + Accessible* widget = ContainerWidget(); + if (widget && widget->CurrentItem() == this) + state |= states::ACTIVE; + } + + if ((state & states::COLLAPSED) || (state & states::EXPANDED)) + state |= states::EXPANDABLE; + + // For some reasons DOM node may have not a frame. We tract such accessibles + // as invisible. + nsIFrame *frame = GetFrame(); + if (!frame) + return state; + + if (frame->StyleEffects()->mOpacity == 1.0f && + !(state & states::INVISIBLE)) { + state |= states::OPAQUE1; + } + + return state; +} + +void +Accessible::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 Accessible* ancestor = this; + while ((ancestor = ancestor->Parent()) && !ancestor->IsDoc()) { + dom::Element* el = ancestor->Elm(); + if (el && + el->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_activedescendant)) { + *aState |= states::FOCUSABLE; + break; + } + } + } + } + + if (*aState & states::FOCUSABLE) { + // Propogate aria-disabled from ancestors down to any focusable descendant. + const Accessible* ancestor = this; + while ((ancestor = ancestor->Parent()) && !ancestor->IsDoc()) { + dom::Element* el = ancestor->Elm(); + if (el && el->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_disabled, + nsGkAtoms::_true, eCaseMatters)) { + *aState |= states::UNAVAILABLE; + break; + } + } + } + + // 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 editable/readonly states from the grid until it's + // overridden. + if ((roleMapEntry->Is(nsGkAtoms::gridcell) || + roleMapEntry->Is(nsGkAtoms::columnheader) || + roleMapEntry->Is(nsGkAtoms::rowheader)) && + !(*aState & (states::READONLY | states::EDITABLE))) { + const TableCellAccessible* cell = AsTableCell(); + if (cell) { + TableAccessible* table = cell->Table(); + if (table) { + Accessible* grid = table->AsAccessible(); + uint64_t gridState = 0; + grid->ApplyARIAState(&gridState); + *aState |= (gridState & (states::READONLY | states::EDITABLE)); + } + } + } +} + +void +Accessible::Value(nsString& aValue) +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry) + return; + + if (roleMapEntry->valueRule != eNoValue) { + // 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->GetAttr(kNameSpaceID_None, + nsGkAtoms::aria_valuetext, aValue)) { + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow, + aValue); + } + 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)) { + Accessible* option = CurrentItem(); + if (!option) { + uint32_t childCount = ChildCount(); + for (uint32_t idx = 0; idx < childCount; idx++) { + Accessible* child = mChildren.ElementAt(idx); + if (child->IsListControl()) { + option = child->GetSelectedItem(0); + break; + } + } + } + + if (option) + nsTextEquivUtils::GetTextEquivFromSubtree(option, aValue); + } +} + +double +Accessible::MaxValue() const +{ + return AttrNumericValue(nsGkAtoms::aria_valuemax); +} + +double +Accessible::MinValue() const +{ + return AttrNumericValue(nsGkAtoms::aria_valuemin); +} + +double +Accessible::Step() const +{ + return UnspecifiedNaN<double>(); // no mimimum increment (step) in ARIA. +} + +double +Accessible::CurValue() const +{ + return AttrNumericValue(nsGkAtoms::aria_valuenow); +} + +bool +Accessible::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); + + return NS_SUCCEEDED( + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow, strValue, true)); +} + +role +Accessible::ARIATransformRole(role 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->AttrValueIs(kNameSpaceID_None, + 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->Role() == roles::COMBOBOX) { + return roles::COMBOBOX_LIST; + } else { + // Listbox is owned by a combobox + Relation rel = RelationByType(RelationType::NODE_CHILD_OF); + Accessible* targetAcc = nullptr; + while ((targetAcc = rel.Next())) + if (targetAcc->Role() == roles::COMBOBOX) + 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->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_haspopup, + nsGkAtoms::_true, eCaseMatters)) { + return roles::PARENT_MENUITEM; + } + } + + return aRole; +} + +nsIAtom* +Accessible::LandmarkRole() const +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return roleMapEntry && roleMapEntry->IsOfType(eLandmark) ? + *(roleMapEntry->roleAtom) : nullptr; +} + +role +Accessible::NativeRole() +{ + return roles::NOTHING; +} + +uint8_t +Accessible::ActionCount() +{ + return GetActionRule() == eNoAction ? 0 : 1; +} + +void +Accessible::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; + } +} + +bool +Accessible::DoAction(uint8_t aIndex) +{ + if (aIndex != 0) + return false; + + if (GetActionRule() != eNoAction) { + DoCommand(); + return true; + } + + return false; +} + +nsIContent* +Accessible::GetAtomicRegion() const +{ + nsIContent *loopContent = mContent; + nsAutoString atomic; + while (loopContent && !loopContent->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_atomic, atomic)) + loopContent = loopContent->GetParent(); + + return atomic.EqualsLiteral("true") ? loopContent : nullptr; +} + +Relation +Accessible::RelationByType(RelationType aType) +{ + 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)); + } else if (mContent->IsXULElement()) { + 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)) { + rel.AppendTarget(GetGroupInfo()->ConceptualParent()); + } + + // 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(Parent()); + } + } + + 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: + return Relation(mDoc, GetAtomicRegion()); + + 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) { + nsCOMPtr<nsIForm> form(do_QueryInterface(control->GetFormElement())); + if (form) { + nsCOMPtr<nsIContent> formContent = + do_QueryInterface(form->GetDefaultSubmitElement()); + return Relation(mDoc, formContent); + } + } + } else { + // In XUL, use first <button default="true" .../> in the document + nsCOMPtr<nsIDOMXULDocument> xulDoc = + do_QueryInterface(mContent->OwnerDoc()); + nsCOMPtr<nsIDOMXULButtonElement> buttonEl; + if (xulDoc) { + nsCOMPtr<nsIDOMNodeList> possibleDefaultButtons; + xulDoc->GetElementsByAttribute(NS_LITERAL_STRING("default"), + NS_LITERAL_STRING("true"), + getter_AddRefs(possibleDefaultButtons)); + if (possibleDefaultButtons) { + uint32_t length; + possibleDefaultButtons->GetLength(&length); + nsCOMPtr<nsIDOMNode> possibleButton; + // Check for button in list of default="true" elements + for (uint32_t count = 0; count < length && !buttonEl; count ++) { + possibleDefaultButtons->Item(count, getter_AddRefs(possibleButton)); + buttonEl = do_QueryInterface(possibleButton); + } + } + if (!buttonEl) { // Check for anonymous accept button in <dialog> + dom::Element* rootElm = mContent->OwnerDoc()->GetRootElement(); + if (rootElm) { + nsIContent* possibleButtonEl = rootElm->OwnerDoc()-> + GetAnonymousElementByAttribute(rootElm, nsGkAtoms::_default, + NS_LITERAL_STRING("true")); + buttonEl = do_QueryInterface(possibleButtonEl); + } + } + nsCOMPtr<nsIContent> relatedContent(do_QueryInterface(buttonEl)); + return Relation(mDoc, relatedContent); + } + } + 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->GetSameTypeRootTreeItem(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 +Accessible::GetNativeInterface(void** aNativeAccessible) +{ +} + +void +Accessible::DoCommand(nsIContent *aContent, uint32_t aActionIndex) +{ + class Runnable final : public mozilla::Runnable + { + public: + Runnable(Accessible* aAcc, nsIContent* aContent, uint32_t aIdx) : + mAcc(aAcc), mContent(aContent), mIdx(aIdx) { } + + NS_IMETHOD Run() override + { + if (mAcc) + mAcc->DispatchClickEvent(mContent, mIdx); + + return NS_OK; + } + + void Revoke() + { + mAcc = nullptr; + mContent = nullptr; + } + + private: + RefPtr<Accessible> 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 +Accessible::DispatchClickEvent(nsIContent *aContent, uint32_t aActionIndex) +{ + if (IsDefunct()) + return; + + nsCOMPtr<nsIPresShell> presShell = mDoc->PresShell(); + + // Scroll into view. + presShell->ScrollContentIntoView(aContent, + nsIPresShell::ScrollAxis(), + nsIPresShell::ScrollAxis(), + nsIPresShell::SCROLL_OVERFLOW_HIDDEN); + + nsWeakFrame 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 +Accessible::ScrollToPoint(uint32_t aCoordinateType, int32_t aX, int32_t aY) +{ + nsIFrame* frame = GetFrame(); + if (!frame) + return; + + nsIntPoint coords = + nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this); + + nsIFrame* parentFrame = frame; + while ((parentFrame = parentFrame->GetParent())) + nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords); +} + +void +Accessible::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; + + nsIFrame *frame = GetFrame(); + if (!frame) + return; + + NS_ASSERTION(mParent, + "Called on accessible unbound from tree. Result can be wrong."); + + if (frame->GetType() == nsGkAtoms::brFrame) { + 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 +Accessible::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(); +} + +// Accessible protected +void +Accessible::ARIAName(nsString& aName) +{ + // 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->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_label, aName)) { + aName.CompressWhitespace(); + } +} + +// Accessible protected +ENameValueFlag +Accessible::NativeName(nsString& aName) +{ + if (mContent->IsHTMLElement()) { + Accessible* label = nullptr; + HTMLLabelIterator iter(Document(), this); + while ((label = iter.Next())) { + nsTextEquivUtils::AppendTextEquivFromContent(this, label->GetContent(), + &aName); + aName.CompressWhitespace(); + } + + 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; +} + +// Accessible protected +void +Accessible::NativeDescription(nsString& aDescription) +{ + bool isXUL = mContent->IsXULElement(); + if (isXUL) { + // Try XUL <description control="[id]">description text</description> + XULDescriptionIterator iter(Document(), mContent); + Accessible* descr = nullptr; + while ((descr = iter.Next())) { + nsTextEquivUtils::AppendTextEquivFromContent(this, descr->GetContent(), + &aDescription); + } + } +} + +// Accessible protected +void +Accessible::BindToParent(Accessible* 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; + + // Note: this is currently only used for richlistitems and their children. + if (mParent->HasNameDependentParent() || mParent->IsXULListItem()) + mContextFlags |= eHasNameDependentParent; + else + mContextFlags &= ~eHasNameDependentParent; + + if (mParent->IsARIAHidden() || aria::HasDefinedARIAHidden(mContent)) + SetARIAHidden(true); + + mContextFlags |= + static_cast<uint32_t>((mParent->IsAlert() || + mParent->IsInsideAlert())) & eInsideAlert; +} + +// Accessible protected +void +Accessible::UnbindFromParent() +{ + mParent = nullptr; + mIndexInParent = -1; + mInt.mIndexOfEmbeddedChild = -1; + if (IsProxy()) + MOZ_CRASH("this should never be called on proxy wrappers"); + + delete mBits.groupInfo; + mBits.groupInfo = nullptr; + mContextFlags &= ~eHasNameDependentParent & ~eInsideAlert; +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible public methods + +RootAccessible* +Accessible::RootAccessible() const +{ + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode()); + NS_ASSERTION(docShell, "No docshell for mContent"); + if (!docShell) { + return nullptr; + } + + nsCOMPtr<nsIDocShellTreeItem> root; + docShell->GetRootTreeItem(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* +Accessible::GetFrame() const +{ + return mContent ? mContent->GetPrimaryFrame() : nullptr; +} + +nsINode* +Accessible::GetNode() const +{ + return mContent; +} + +void +Accessible::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 +Accessible::InsertChildAt(uint32_t aIndex, Accessible* aChild) +{ + if (!aChild) + return false; + + if (aIndex == mChildren.Length()) { + if (!mChildren.AppendElement(aChild)) + return false; + + } else { + if (!mChildren.InsertElementAt(aIndex, aChild)) + return false; + + 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 +Accessible::RemoveChild(Accessible* aChild) +{ + if (!aChild) + return false; + + if (aChild->mParent != this || aChild->mIndexInParent == -1) + return false; + + MOZ_ASSERT((mStateFlags & eKidsMutating) || aChild->IsDefunct() || aChild->IsDoc(), + "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 +Accessible::MoveChild(uint32_t aNewIndex, Accessible* aChild) +{ + MOZ_ASSERT(aChild, "No child was given"); + MOZ_ASSERT(aChild->mParent == this, "A child from different subtree was given"); + MOZ_ASSERT(aChild->mIndexInParent != -1, "Unbound child was given"); + MOZ_ASSERT(static_cast<uint32_t>(aChild->mIndexInParent) != aNewIndex, + "No move, same index"); + MOZ_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]->mStateFlags |= eGroupInfoDirty; + mChildren[idx]->mInt.mIndexOfEmbeddedChild = -1; + } + + RefPtr<AccShowEvent> showEvent = new AccShowEvent(aChild); + DebugOnly<bool> added = mDoc->Controller()->QueueMutationEvent(showEvent); + MOZ_ASSERT(added); + aChild->SetShowEventTarget(true); +} + +Accessible* +Accessible::GetChildAt(uint32_t aIndex) const +{ + Accessible* child = mChildren.SafeElementAt(aIndex, nullptr); + if (!child) + return nullptr; + +#ifdef DEBUG + Accessible* realParent = child->mParent; + NS_ASSERTION(!realParent || realParent == this, + "Two accessibles have the same first child accessible!"); +#endif + + return child; +} + +uint32_t +Accessible::ChildCount() const +{ + return mChildren.Length(); +} + +int32_t +Accessible::IndexInParent() const +{ + return mIndexInParent; +} + +uint32_t +Accessible::EmbeddedChildCount() +{ + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + return mEmbeddedObjCollector->Count(); + } + + return ChildCount(); +} + +Accessible* +Accessible::GetEmbeddedChildAt(uint32_t aIndex) +{ + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + return mEmbeddedObjCollector.get() ? + mEmbeddedObjCollector->GetAccessibleAt(aIndex) : nullptr; + } + + return GetChildAt(aIndex); +} + +int32_t +Accessible::GetIndexOfEmbeddedChild(Accessible* aChild) +{ + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + return mEmbeddedObjCollector.get() ? + mEmbeddedObjCollector->GetIndexAt(aChild) : -1; + } + + return GetIndexOf(aChild); +} + +//////////////////////////////////////////////////////////////////////////////// +// HyperLinkAccessible methods + +bool +Accessible::IsLink() +{ + // Every embedded accessible within hypertext accessible implements + // hyperlink interface. + return mParent && mParent->IsHyperText() && !IsText(); +} + +uint32_t +Accessible::StartOffset() +{ + NS_PRECONDITION(IsLink(), "StartOffset is called not on hyper link!"); + + HyperTextAccessible* hyperText = mParent ? mParent->AsHyperText() : nullptr; + return hyperText ? hyperText->GetChildOffset(this) : 0; +} + +uint32_t +Accessible::EndOffset() +{ + NS_PRECONDITION(IsLink(), "EndOffset is called on not hyper link!"); + + HyperTextAccessible* hyperText = mParent ? mParent->AsHyperText() : nullptr; + return hyperText ? (hyperText->GetChildOffset(this) + 1) : 0; +} + +uint32_t +Accessible::AnchorCount() +{ + NS_PRECONDITION(IsLink(), "AnchorCount is called on not hyper link!"); + return 1; +} + +Accessible* +Accessible::AnchorAt(uint32_t aAnchorIndex) +{ + NS_PRECONDITION(IsLink(), "GetAnchor is called on not hyper link!"); + return aAnchorIndex == 0 ? this : nullptr; +} + +already_AddRefed<nsIURI> +Accessible::AnchorURIAt(uint32_t aAnchorIndex) +{ + NS_PRECONDITION(IsLink(), "AnchorURIAt is called on not hyper link!"); + return nullptr; +} + +void +Accessible::ToTextPoint(HyperTextAccessible** aContainer, int32_t* aOffset, + bool aIsBefore) const +{ + if (IsHyperText()) { + *aContainer = const_cast<Accessible*>(this)->AsHyperText(); + *aOffset = aIsBefore ? 0 : (*aContainer)->CharacterCount(); + return; + } + + const Accessible* child = nullptr; + const Accessible* parent = this; + do { + child = parent; + parent = parent->Parent(); + } while (parent && !parent->IsHyperText()); + + if (parent) { + *aContainer = const_cast<Accessible*>(parent)->AsHyperText(); + *aOffset = (*aContainer)->GetChildOffset( + child->IndexInParent() + static_cast<int32_t>(!aIsBefore)); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// SelectAccessible + +void +Accessible::SelectedItems(nsTArray<Accessible*>* aItems) +{ + AccIterator iter(this, filters::GetSelected); + Accessible* selected = nullptr; + while ((selected = iter.Next())) + aItems->AppendElement(selected); +} + +uint32_t +Accessible::SelectedItemCount() +{ + uint32_t count = 0; + AccIterator iter(this, filters::GetSelected); + Accessible* selected = nullptr; + while ((selected = iter.Next())) + ++count; + + return count; +} + +Accessible* +Accessible::GetSelectedItem(uint32_t aIndex) +{ + AccIterator iter(this, filters::GetSelected); + Accessible* selected = nullptr; + + uint32_t index = 0; + while ((selected = iter.Next()) && index < aIndex) + index++; + + return selected; +} + +bool +Accessible::IsItemSelected(uint32_t aIndex) +{ + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + Accessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) + index++; + + return selected && + selected->State() & states::SELECTED; +} + +bool +Accessible::AddItemToSelection(uint32_t aIndex) +{ + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + Accessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) + index++; + + if (selected) + selected->SetSelected(true); + + return static_cast<bool>(selected); +} + +bool +Accessible::RemoveItemFromSelection(uint32_t aIndex) +{ + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + Accessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) + index++; + + if (selected) + selected->SetSelected(false); + + return static_cast<bool>(selected); +} + +bool +Accessible::SelectAll() +{ + bool success = false; + Accessible* selectable = nullptr; + + AccIterator iter(this, filters::GetSelectable); + while((selectable = iter.Next())) { + success = true; + selectable->SetSelected(true); + } + return success; +} + +bool +Accessible::UnselectAll() +{ + bool success = false; + Accessible* selected = nullptr; + + AccIterator iter(this, filters::GetSelected); + while ((selected = iter.Next())) { + success = true; + selected->SetSelected(false); + } + return success; +} + +//////////////////////////////////////////////////////////////////////////////// +// Widgets + +bool +Accessible::IsWidget() const +{ + return false; +} + +bool +Accessible::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++) { + Accessible* child = mChildren.ElementAt(idx); + if (child->Role() == roles::ENTRY) + return FocusMgr()->HasDOMFocus(child->GetContent()); + } + } + + return false; +} + +bool +Accessible::AreItemsOperable() const +{ + return HasOwnContent() && + mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_activedescendant); +} + +Accessible* +Accessible::CurrentItem() +{ + // 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->GetAttr(kNameSpaceID_None, + nsGkAtoms::aria_activedescendant, id)) { + nsIDocument* DOMDoc = mContent->OwnerDoc(); + dom::Element* activeDescendantElm = DOMDoc->GetElementById(id); + if (activeDescendantElm) { + DocAccessible* document = Document(); + if (document) + return document->GetAccessible(activeDescendantElm); + } + } + return nullptr; +} + +void +Accessible::SetCurrentItem(Accessible* aItem) +{ + nsIAtom* id = aItem->GetContent()->GetID(); + if (id) { + nsAutoString idStr; + id->ToString(idStr); + mContent->SetAttr(kNameSpaceID_None, + nsGkAtoms::aria_activedescendant, idStr, true); + } +} + +Accessible* +Accessible::ContainerWidget() const +{ + if (HasARIARole() && mContent->HasID()) { + for (Accessible* parent = Parent(); parent; parent = parent->Parent()) { + nsIContent* parentContent = parent->GetContent(); + if (parentContent && + parentContent->HasAttr(kNameSpaceID_None, + nsGkAtoms::aria_activedescendant)) { + return parent; + } + + // Don't cross DOM document boundaries. + if (parent->IsDoc()) + break; + } + } + return nullptr; +} + +void +Accessible::SetARIAHidden(bool aIsDefined) +{ + if (aIsDefined) + mContextFlags |= eARIAHidden; + else + mContextFlags &= ~eARIAHidden; + + uint32_t length = mChildren.Length(); + for (uint32_t i = 0; i < length; i++) { + mChildren[i]->SetARIAHidden(aIsDefined); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible protected methods + +void +Accessible::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; +} + +Accessible* +Accessible::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; + } + + Accessible* child = mParent->GetChildAt(mIndexInParent + aOffset); + if (aError && !child) + *aError = NS_ERROR_UNEXPECTED; + + return child; +} + +double +Accessible::AttrNumericValue(nsIAtom* aAttr) const +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) + return UnspecifiedNaN<double>(); + + nsAutoString attrValue; + if (!mContent->GetAttr(kNameSpaceID_None, aAttr, attrValue)) + return UnspecifiedNaN<double>(); + + nsresult error = NS_OK; + double value = attrValue.ToDouble(&error); + return NS_FAILED(error) ? UnspecifiedNaN<double>() : value; +} + +uint32_t +Accessible::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->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* +Accessible::GetGroupInfo() +{ + if (IsProxy()) + MOZ_CRASH("This should never be called on proxy wrappers"); + + if (mBits.groupInfo){ + if (HasDirtyGroupInfo()) { + mBits.groupInfo->Update(); + mStateFlags &= ~eGroupInfoDirty; + } + + return mBits.groupInfo; + } + + mBits.groupInfo = AccGroupInfo::CreateGroupInfo(this); + return mBits.groupInfo; +} + +void +Accessible::GetPositionAndSizeInternal(int32_t *aPosInSet, int32_t *aSetSize) +{ + AccGroupInfo* groupInfo = GetGroupInfo(); + if (groupInfo) { + *aPosInSet = groupInfo->PosInSet(); + *aSetSize = groupInfo->SetSize(); + } +} + +int32_t +Accessible::GetLevelInternal() +{ + int32_t level = nsAccUtils::GetDefaultLevel(this); + + if (!IsBoundToParent()) + return level; + + roles::Role role = Role(); + if (role == roles::OUTLINEITEM) { + // Always expose 'level' attribute for 'outlineitem' accessible. The number + // of nested 'grouping' accessibles containing 'outlineitem' accessible is + // its level. + level = 1; + + Accessible* parent = this; + while ((parent = parent->Parent())) { + roles::Role parentRole = parent->Role(); + + if (parentRole == roles::OUTLINE) + break; + if (parentRole == roles::GROUPING) + ++ level; + + } + + } else if (role == roles::LISTITEM) { + // Expose 'level' attribute on nested lists. We support two hierarchies: + // a) list -> listitem -> list -> listitem (nested list is a last child + // of listitem of the parent list); + // b) list -> listitem -> group -> listitem (nested listitems are contained + // by group that is a last child of the parent listitem). + + // Calculate 'level' attribute based on number of parent listitems. + level = 0; + Accessible* parent = this; + while ((parent = parent->Parent())) { + roles::Role parentRole = parent->Role(); + + if (parentRole == roles::LISTITEM) + ++ level; + else if (parentRole != roles::LIST && parentRole != roles::GROUPING) + break; + } + + if (level == 0) { + // If this listitem is on top of nested lists then expose 'level' + // attribute. + parent = Parent(); + uint32_t siblingCount = parent->ChildCount(); + for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) { + Accessible* sibling = parent->GetChildAt(siblingIdx); + + Accessible* siblingChild = sibling->LastChild(); + if (siblingChild) { + roles::Role lastChildRole = siblingChild->Role(); + if (lastChildRole == roles::LIST || lastChildRole == roles::GROUPING) + return 1; + } + } + } else { + ++ level; // level is 1-index based + } + } + + return level; +} + +void +Accessible::StaticAsserts() const +{ + static_assert(eLastStateFlag <= (1 << kStateFlagsBits) - 1, + "Accessible::mStateFlags was oversized by eLastStateFlag!"); + static_assert(eLastAccType <= (1 << kTypeBits) - 1, + "Accessible::mType was oversized by eLastAccType!"); + static_assert(eLastContextFlag <= (1 << kContextFlagsBits) - 1, + "Accessible::mContextFlags was oversized by eLastContextFlag!"); + static_assert(eLastAccGenericType <= (1 << kGenericTypesBits) - 1, + "Accessible::mGenericType was oversized by eLastAccGenericType!"); +} + +//////////////////////////////////////////////////////////////////////////////// +// KeyBinding class + +// static +uint32_t +KeyBinding::AccelModifier() +{ + switch (WidgetInputEvent::AccelModifier()) { + case MODIFIER_ALT: + return kAlt; + case MODIFIER_CONTROL: + return kControl; + case MODIFIER_META: + return kMeta; + case MODIFIER_OS: + return kOS; + default: + MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()"); + return 0; + } +} + +void +KeyBinding::ToPlatformFormat(nsAString& aValue) const +{ + nsCOMPtr<nsIStringBundle> keyStringBundle; + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::services::GetStringBundleService(); + if (stringBundleService) + stringBundleService->CreateBundle( + "chrome://global-platform/locale/platformKeys.properties", + getter_AddRefs(keyStringBundle)); + + if (!keyStringBundle) + return; + + nsAutoString separator; + keyStringBundle->GetStringFromName(u"MODIFIER_SEPARATOR", + getter_Copies(separator)); + + nsAutoString modifierName; + if (mModifierMask & kControl) { + keyStringBundle->GetStringFromName(u"VK_CONTROL", + getter_Copies(modifierName)); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kAlt) { + keyStringBundle->GetStringFromName(u"VK_ALT", + getter_Copies(modifierName)); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kShift) { + keyStringBundle->GetStringFromName(u"VK_SHIFT", + getter_Copies(modifierName)); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kMeta) { + keyStringBundle->GetStringFromName(u"VK_META", + getter_Copies(modifierName)); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + aValue.Append(mKey); +} + +void +KeyBinding::ToAtkFormat(nsAString& aValue) const +{ + nsAutoString modifierName; + if (mModifierMask & kControl) + aValue.AppendLiteral("<Control>"); + + if (mModifierMask & kAlt) + aValue.AppendLiteral("<Alt>"); + + if (mModifierMask & kShift) + aValue.AppendLiteral("<Shift>"); + + if (mModifierMask & kMeta) + aValue.AppendLiteral("<Meta>"); + + aValue.Append(mKey); +} diff --git a/accessible/generic/Accessible.h b/accessible/generic/Accessible.h new file mode 100644 index 0000000000..eaf041920b --- /dev/null +++ b/accessible/generic/Accessible.h @@ -0,0 +1,1269 @@ +/* -*- 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 _Accessible_H_ +#define _Accessible_H_ + +#include "mozilla/a11y/AccTypes.h" +#include "mozilla/a11y/RelationType.h" +#include "mozilla/a11y/Role.h" +#include "mozilla/a11y/States.h" + +#include "mozilla/UniquePtr.h" + +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsRefPtrHashtable.h" +#include "nsRect.h" + +struct nsRoleMapEntry; + +struct nsRect; +class nsIFrame; +class nsIAtom; +class nsIPersistentProperties; + +namespace mozilla { +namespace a11y { + +class Accessible; +class AccEvent; +class AccGroupInfo; +class ApplicationAccessible; +class DocAccessible; +class EmbeddedObjCollector; +class EventTree; +class HTMLImageMapAccessible; +class HTMLLIAccessible; +class HyperTextAccessible; +class ImageAccessible; +class KeyBinding; +class OuterDocAccessible; +class ProxyAccessible; +class Relation; +class RootAccessible; +class TableAccessible; +class TableCellAccessible; +class TextLeafAccessible; +class XULLabelAccessible; +class XULTreeAccessible; + +#ifdef A11Y_LOG +namespace logging { + typedef const char* (*GetTreePrefix)(void* aData, Accessible*); + void Tree(const char* aTitle, const char* aMsgText, Accessible* aRoot, + GetTreePrefix aPrefixFunc, void* GetTreePrefixData); +}; +#endif + +/** + * Name type flags. + */ +enum ENameValueFlag { + /** + * Name either + * a) present (not empty): !name.IsEmpty() + * b) no name (was missed): name.IsVoid() + */ + eNameOK, + + /** + * Name was left empty by the author on purpose: + * name.IsEmpty() && !name.IsVoid(). + */ + eNoNameOnPurpose, + + /** + * Name was computed from the subtree. + */ + eNameFromSubtree, + + /** + * Tooltip was used as a name. + */ + eNameFromTooltip +}; + +/** + * Group position (level, position in set and set size). + */ +struct GroupPos +{ + GroupPos() : level(0), posInSet(0), setSize(0) { } + GroupPos(int32_t aLevel, int32_t aPosInSet, int32_t aSetSize) : + level(aLevel), posInSet(aPosInSet), setSize(aSetSize) { } + + int32_t level; + int32_t posInSet; + int32_t setSize; +}; + +/** + * An index type. Assert if out of range value was attempted to be used. + */ +class index_t +{ +public: + MOZ_IMPLICIT index_t(int32_t aVal) : mVal(aVal) {} + + operator uint32_t() const + { + MOZ_ASSERT(mVal >= 0, "Attempt to use wrong index!"); + return mVal; + } + + bool IsValid() const { return mVal >= 0; } + +private: + int32_t mVal; +}; + +typedef nsRefPtrHashtable<nsPtrHashKey<const void>, Accessible> + AccessibleHashtable; + + +#define NS_ACCESSIBLE_IMPL_IID \ +{ /* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \ + 0x133c8bf4, \ + 0x4913, \ + 0x4355, \ + { 0xbd, 0x50, 0x42, 0x6b, 0xd1, 0xd6, 0xe1, 0xad } \ +} + +class Accessible : public nsISupports +{ +public: + Accessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(Accessible) + + 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. + */ + virtual nsIFrame* GetFrame() const; + + /** + * Return DOM node associated with the accessible. + */ + virtual nsINode* GetNode() const; + inline already_AddRefed<nsIDOMNode> DOMNode() const + { + nsCOMPtr<nsIDOMNode> DOMNode = do_QueryInterface(GetNode()); + return DOMNode.forget(); + } + nsIContent* GetContent() const { return mContent; } + mozilla::dom::Element* Elm() const + { return mContent && mContent->IsElement() ? mContent->AsElement() : nullptr; } + + /** + * Return node type information of DOM node associated with the accessible. + */ + bool IsContent() const + { return GetNode() && GetNode()->IsNodeOfType(nsINode::eCONTENT); } + + /** + * Return the unique identifier of the accessible. + */ + void* UniqueID() { return static_cast<void*>(this); } + + /** + * Return language associated with the accessible. + */ + void Language(nsAString& aLocale); + + /** + * Get the description of this accessible. + */ + virtual void Description(nsString& aDescription); + + /** + * Get the value of this accessible. + */ + virtual void Value(nsString& aValue); + + /** + * Get help string for the accessible. + */ + void Help(nsString& aHelp) const { aHelp.Truncate(); } + + /** + * Get the name of this accessible. + * + * Note: aName.IsVoid() when name was left empty by the author on purpose. + * aName.IsEmpty() when the author missed name, AT can try to repair a name. + */ + virtual ENameValueFlag Name(nsString& aName); + + /** + * 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). + */ + mozilla::a11y::role Role(); + + /** + * Return true if ARIA role is specified on the element. + */ + bool HasARIARole() const; + bool IsARIARole(nsIAtom* aARIARole) const; + bool HasStrongARIARole() const; + + /** + * Retrun ARIA role map if any. + */ + const nsRoleMapEntry* ARIARoleMap() const; + + /** + * Return accessible role specified by ARIA (see constants in + * roles). + */ + mozilla::a11y::role ARIARole(); + + /** + * Return a landmark role if applied. + */ + virtual nsIAtom* LandmarkRole() const; + + /** + * Returns enumerated accessible role from native markup (see constants in + * Role.h). Doesn't take into account ARIA roles. + */ + virtual mozilla::a11y::role NativeRole(); + + /** + * Return all states of accessible (including ARIA states). + */ + virtual uint64_t State(); + + /** + * 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 if accessible is unavailable. + */ + bool Unavailable() const + { + uint64_t state = NativelyUnavailable() ? states::UNAVAILABLE : 0; + ApplyARIAState(&state); + return state & states::UNAVAILABLE; + } + + /** + * Return the states of accessible, not taking into account ARIA states. + * Use State() to get complete set of states. + */ + virtual uint64_t NativeState(); + + /** + * 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(); + + /** + * Return true if native unavailable state present. + */ + virtual bool NativelyUnavailable() const; + + /** + * Return object attributes for the accessible. + */ + virtual already_AddRefed<nsIPersistentProperties> Attributes(); + + /** + * Return group position (level, position in set and set size). + */ + virtual mozilla::a11y::GroupPos GroupPosition(); + + /** + * Used by ChildAtPoint() method to get direct or deepest child at point. + */ + enum EWhichChildAtPoint { + eDirectChild, + eDeepestChild + }; + + /** + * 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 Accessible* ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild); + + /** + * Return the focused child if any. + */ + virtual Accessible* FocusedChild(); + + /** + * Return calculated group level based on accessible hierarchy. + */ + virtual int32_t GetLevelInternal(); + + /** + * Calculate position in group and group size ('posinset' and 'setsize') based + * on accessible hierarchy. + * + * @param aPosInSet [out] accessible position in the group + * @param aSetSize [out] the group size + */ + virtual void GetPositionAndSizeInternal(int32_t *aPosInSet, + int32_t *aSetSize); + + /** + * Get the relation of the given type. + */ + virtual Relation RelationByType(RelationType aType); + + ////////////////////////////////////////////////////////////////////////////// + // 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(Accessible* aChild) + { return InsertChildAt(mChildren.Length(), aChild); } + virtual bool InsertChildAt(uint32_t aIndex, Accessible* 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(Accessible* aNewChild, Accessible* aRefChild); + + virtual bool RemoveChild(Accessible* aChild); + + /** + * Reallocates the child withing its parent. + */ + void MoveChild(uint32_t aNewIndex, Accessible* aChild); + + ////////////////////////////////////////////////////////////////////////////// + // Accessible tree traverse methods + + /** + * Return parent accessible. + */ + Accessible* Parent() const { return mParent; } + + /** + * Return child accessible at the given index. + */ + virtual Accessible* GetChildAt(uint32_t aIndex) const; + + /** + * Return child accessible count. + */ + virtual uint32_t ChildCount() const; + + /** + * Return index of the given child accessible. + */ + int32_t GetIndexOf(const Accessible* aChild) const + { return (aChild->mParent != this) ? -1 : aChild->IndexInParent(); } + + /** + * Return index in parent accessible. + */ + virtual int32_t IndexInParent() const; + + /** + * Return true if accessible has children; + */ + bool HasChildren() { return !!GetChildAt(0); } + + /** + * Return first/last/next/previous sibling of the accessible. + */ + inline Accessible* NextSibling() const + { return GetSiblingAtOffset(1); } + inline Accessible* PrevSibling() const + { return GetSiblingAtOffset(-1); } + inline Accessible* FirstChild() + { return GetChildAt(0); } + inline Accessible* LastChild() + { + uint32_t childCount = ChildCount(); + return childCount != 0 ? GetChildAt(childCount - 1) : nullptr; + } + + /** + * Return embedded accessible children count. + */ + uint32_t EmbeddedChildCount(); + + /** + * Return embedded accessible child at the given index. + */ + Accessible* GetEmbeddedChildAt(uint32_t aIndex); + + /** + * Return index of the given embedded accessible child. + */ + int32_t GetIndexOfEmbeddedChild(Accessible* aChild); + + /** + * 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(); } + Accessible* 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 true; } + + /** + * Returns text of accessible if accessible has text role otherwise empty + * string. + * + * @param aText [in] returned text of the accessible + * @param aStartOffset [in, optional] start offset inside of the accessible, + * if missed entire text is appended + * @param aLength [in, optional] required length of text, if missed + * then text form start offset till the end is appended + */ + virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0, + uint32_t aLength = UINT32_MAX); + + /** + * Return boundaries in screen coordinates. + */ + virtual nsIntRect Bounds() const; + + /** + * Return boundaries rect relative the bounding frame. + */ + virtual nsRect RelativeBounds(nsIFrame** aRelativeFrame) const; + + /** + * Selects the accessible within its container if applicable. + */ + virtual void SetSelected(bool aSelect); + + /** + * Select the accessible within its container. + */ + void TakeSelection(); + + /** + * Focus the accessible. + */ + virtual void TakeFocus(); + + /** + * Scroll the accessible into view. + */ + void ScrollTo(uint32_t aHow) const; + + /** + * 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); + + ////////////////////////////////////////////////////////////////////////////// + // Downcasting and types + + inline bool IsAbbreviation() const + { + return mContent->IsAnyOfHTMLElements(nsGkAtoms::abbr, nsGkAtoms::acronym); + } + + bool IsAlert() const { return HasGenericType(eAlert); } + + bool IsApplication() const { return mType == eApplicationType; } + ApplicationAccessible* AsApplication(); + + bool IsAutoComplete() const { return HasGenericType(eAutoComplete); } + + bool IsAutoCompletePopup() const + { return HasGenericType(eAutoCompletePopup); } + + bool IsButton() const { return HasGenericType(eButton); } + + bool IsCombobox() const { return HasGenericType(eCombobox); } + + bool IsDoc() const { return HasGenericType(eDocument); } + DocAccessible* AsDoc(); + + bool IsGenericHyperText() const { return mType == eHyperTextType; } + bool IsHyperText() const { return HasGenericType(eHyperText); } + HyperTextAccessible* AsHyperText(); + + bool IsHTMLBr() const { return mType == eHTMLBRType; } + bool IsHTMLCaption() const { return mType == eHTMLCaptionType; } + bool IsHTMLCombobox() const { return mType == eHTMLComboboxType; } + bool IsHTMLFileInput() const { return mType == eHTMLFileInputType; } + + bool IsHTMLListItem() const { return mType == eHTMLLiType; } + HTMLLIAccessible* AsHTMLListItem(); + + bool IsHTMLOptGroup() const { return mType == eHTMLOptGroupType; } + + bool IsHTMLTable() const { return mType == eHTMLTableType; } + bool IsHTMLTableRow() const { return mType == eHTMLTableRowType; } + + bool IsImage() const { return mType == eImageType; } + ImageAccessible* AsImage(); + + bool IsImageMap() const { return mType == eImageMapType; } + HTMLImageMapAccessible* AsImageMap(); + + bool IsList() const { return HasGenericType(eList); } + + bool IsListControl() const { return HasGenericType(eListControl); } + + bool IsMenuButton() const { return HasGenericType(eMenuButton); } + + bool IsMenuPopup() const { return mType == eMenuPopupType; } + + bool IsProxy() const { return mType == eProxyType; } + ProxyAccessible* Proxy() const + { + MOZ_ASSERT(IsProxy()); + return mBits.proxy; + } + uint32_t ProxyInterfaces() const + { + MOZ_ASSERT(IsProxy()); + return mInt.mProxyInterfaces; + } + void SetProxyInterfaces(uint32_t aInterfaces) + { + MOZ_ASSERT(IsProxy()); + mInt.mProxyInterfaces = aInterfaces; + } + + bool IsOuterDoc() const { return mType == eOuterDocType; } + OuterDocAccessible* AsOuterDoc(); + + bool IsProgress() const { return mType == eProgressType; } + + bool IsRoot() const { return mType == eRootType; } + a11y::RootAccessible* AsRoot(); + + bool IsSearchbox() const; + + bool IsSelect() const { return HasGenericType(eSelect); } + + bool IsTable() const { return HasGenericType(eTable); } + virtual TableAccessible* AsTable() { return nullptr; } + + bool IsTableCell() const { return HasGenericType(eTableCell); } + virtual TableCellAccessible* AsTableCell() { return nullptr; } + const TableCellAccessible* AsTableCell() const + { return const_cast<Accessible*>(this)->AsTableCell(); } + + bool IsTableRow() const { return HasGenericType(eTableRow); } + + bool IsTextField() const { return mType == eHTMLTextFieldType; } + + bool IsText() const { return mGenericTypes & eText; } + + bool IsTextLeaf() const { return mType == eTextLeafType; } + TextLeafAccessible* AsTextLeaf(); + + bool IsXULLabel() const { return mType == eXULLabelType; } + XULLabelAccessible* AsXULLabel(); + + bool IsXULListItem() const { return mType == eXULListItemType; } + + bool IsXULTabpanels() const { return mType == eXULTabpanelsType; } + + bool IsXULTree() const { return mType == eXULTreeType; } + XULTreeAccessible* AsXULTree(); + + /** + * Return true if the accessible belongs to the given accessible type. + */ + bool HasGenericType(AccGenericType aType) const; + + ////////////////////////////////////////////////////////////////////////////// + // ActionAccessible + + /** + * Return the number of actions that can be performed on this accessible. + */ + virtual uint8_t ActionCount(); + + /** + * Return action name at given index. + */ + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName); + + /** + * Default to localized action name. + */ + void ActionDescriptionAt(uint8_t aIndex, nsAString& aDescription) + { + nsAutoString name; + ActionNameAt(aIndex, name); + TranslateString(name, aDescription); + } + + /** + * Invoke the accessible action. + */ + virtual bool DoAction(uint8_t aIndex); + + /** + * Return access key, such as Alt+D. + */ + virtual KeyBinding AccessKey() const; + + /** + * 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(); + + /** + * Return the start offset of the link within the parent accessible. + */ + virtual uint32_t StartOffset(); + + /** + * Return the end offset of the link within the parent accessible. + */ + virtual uint32_t EndOffset(); + + /** + * Return true if the link is valid (e. g. points to a valid URL). + */ + inline bool IsLinkValid() + { + NS_PRECONDITION(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 Accessible* AnchorAt(uint32_t aAnchorIndex); + + /** + * Returns an anchor URI at the given index. + */ + virtual already_AddRefed<nsIURI> AnchorURIAt(uint32_t aAnchorIndex); + + /** + * Returns a text point for the accessible element. + */ + void ToTextPoint(HyperTextAccessible** aContainer, int32_t* aOffset, + bool aIsBefore = true) const; + + ////////////////////////////////////////////////////////////////////////////// + // SelectAccessible + + /** + * Return an array of selected items. + */ + virtual void SelectedItems(nsTArray<Accessible*>* aItems); + + /** + * Return the number of selected items. + */ + virtual uint32_t SelectedItemCount(); + + /** + * Return selected item at the given index. + */ + virtual Accessible* GetSelectedItem(uint32_t aIndex); + + /** + * Determine if item at the given index is selected. + */ + virtual bool IsItemSelected(uint32_t aIndex); + + /** + * Add item at the given index the selection. Return true if success. + */ + virtual bool AddItemToSelection(uint32_t aIndex); + + /** + * Remove item at the given index from the selection. Return if success. + */ + virtual bool RemoveItemFromSelection(uint32_t aIndex); + + /** + * Select all items. Return true if success. + */ + virtual bool SelectAll(); + + /** + * Unselect all items. Return true if success. + */ + virtual bool UnselectAll(); + + ////////////////////////////////////////////////////////////////////////////// + // Value (numeric value interface) + + virtual double MaxValue() const; + virtual double MinValue() const; + virtual double CurValue() const; + virtual double Step() const; + 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 Accessible* CurrentItem(); + + /** + * Set the current item of the widget. + */ + virtual void SetCurrentItem(Accessible* aItem); + + /** + * Return container widget this accessible belongs to. + */ + virtual Accessible* ContainerWidget() const; + + /** + * Return the localized string for the given key. + */ + static void TranslateString(const nsString& aKey, nsAString& aStringOut); + + /** + * Return true if the accessible is defunct. + */ + bool IsDefunct() const { return mStateFlags & eIsDefunct; } + + /** + * 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's group info needs to be updated. + */ + inline bool HasDirtyGroupInfo() const { return mStateFlags & eGroupInfoDirty; } + + /** + * Return true if the accessible has associated DOM content. + */ + bool HasOwnContent() const + { return mContent && !(mStateFlags & eSharedNode); } + + /** + * Return true if the accessible has a numeric value. + */ + bool HasNumericValue() const; + + /** + * Return true if the accessible state change is processed by handling proper + * DOM UI event, if otherwise then false. For example, HTMLCheckboxAccessible + * process nsIDocumentObserver::ContentStateChanged instead + * 'CheckboxStateChange' event. + */ + bool NeedsDOMUIEvent() const + { return !(mStateFlags & eIgnoreDOMUIEvent); } + + /** + * Get/set survivingInUpdate bit on child indicating that parent recollects + * its children. + */ + bool IsSurvivingInUpdate() const { return mStateFlags & eSurvivingInUpdate; } + void SetSurvivingInUpdate(bool aIsSurviving) + { + if (aIsSurviving) + mStateFlags |= eSurvivingInUpdate; + else + mStateFlags &= ~eSurvivingInUpdate; + } + + /** + * 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 doesn't allow accessible children from XBL + * anonymous subtree. + */ + bool NoXBLKids() const { return mStateFlags & eNoXBLKids; } + + /** + * 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 whose name depends on this + * accessible. + */ + bool HasNameDependentParent() const + { return mContextFlags & eHasNameDependentParent; } + + /** + * Return true if aria-hidden="true" is applied to the accessible or inherited + * from the parent. + */ + bool IsARIAHidden() const { return mContextFlags & eARIAHidden; } + void SetARIAHidden(bool aIsDefined); + + /** + * 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; } + +protected: + virtual ~Accessible(); + + /** + * 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); + + /** + * Return the accessible description provided by native markup. It doesn't take + * into account ARIA markup used to specify the description. + */ + virtual void NativeDescription(nsString& aDescription); + + /** + * Return object attributes provided by native markup. It doesn't take into + * account ARIA. + */ + virtual already_AddRefed<nsIPersistentProperties> NativeAttributes(); + + ////////////////////////////////////////////////////////////////////////////// + // Initializing, cache and tree traverse methods + + /** + * Destroy the object. + */ + void LastRelease(); + + /** + * Set accessible parent and index in parent. + */ + void BindToParent(Accessible* aParent, uint32_t aIndexInParent); + void UnbindFromParent(); + + /** + * Return sibling accessible at the given offset. + */ + virtual Accessible* GetSiblingAtOffset(int32_t aOffset, + nsresult *aError = nullptr) const; + + /** + * 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 + eHasNumericValue = 1 << 4, // accessible has a numeric value + eGroupInfoDirty = 1 << 5, // accessible needs to update group info + eKidsMutating = 1 << 6, // subtree is being mutated + eIgnoreDOMUIEvent = 1 << 7, // don't process DOM UI events for a11y events + eSurvivingInUpdate = 1 << 8, // parent drops children to recollect them + eRelocated = 1 << 9, // accessible was moved in tree + eNoXBLKids = 1 << 10, // accessible don't allows XBL children + eNoKidsFromDOM = 1 << 11, // accessible doesn't allow children from DOM + eHasTextKids = 1 << 12, // accessible have a text leaf in children + + eLastStateFlag = eNoKidsFromDOM + }; + + /** + * Flags used for contextual information about the accessible. + */ + enum ContextFlags { + eHasNameDependentParent = 1 << 0, // Parent's name depends on this accessible. + eARIAHidden = 1 << 1, + eInsideAlert = 1 << 2, + + eLastContextFlag = eInsideAlert + }; + +protected: + + ////////////////////////////////////////////////////////////////////////////// + // Miscellaneous helpers + + /** + * Return ARIA role (helper method). + */ + mozilla::a11y::role ARIATransformRole(mozilla::a11y::role aRole); + + ////////////////////////////////////////////////////////////////////////////// + // Name helpers + + /** + * Returns the accessible name specified by ARIA. + */ + void ARIAName(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); + + /** + * Dispatch click event. + */ + virtual void DispatchClickEvent(nsIContent *aContent, uint32_t aActionIndex); + + ////////////////////////////////////////////////////////////////////////////// + // 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(nsIAtom* aARIAAttr) const; + + /** + * Return the action rule based on ARIA enum constants EActionRule + * (see ARIAMap.h). Used by ActionCount() and ActionNameAt(). + */ + uint32_t GetActionRule() const; + + /** + * Return group info. + */ + AccGroupInfo* GetGroupInfo(); + + // Data Members + nsCOMPtr<nsIContent> mContent; + RefPtr<DocAccessible> mDoc; + + Accessible* mParent; + nsTArray<Accessible*> mChildren; + int32_t mIndexInParent; + + static const uint8_t kStateFlagsBits = 13; + static const uint8_t kContextFlagsBits = 3; + static const uint8_t kTypeBits = 6; + static const uint8_t kGenericTypesBits = 16; + + /** + * Non-NO_ROLE_MAP_ENTRY_INDEX indicates author-supplied role; + * possibly state & value as well + */ + uint8_t mRoleMapEntryIndex; + + /** + * Keep in sync with StateFlags, ContextFlags, and AccTypes. + */ + uint32_t mStateFlags : kStateFlagsBits; + uint32_t mContextFlags : kContextFlagsBits; + uint32_t mType : kTypeBits; + uint32_t mGenericTypes : kGenericTypesBits; + 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, + Accessible* aRoot, + logging::GetTreePrefix aPrefixFunc, + void* aGetTreePrefixData); +#endif + friend class DocAccessible; + friend class xpcAccessible; + friend class TreeMutation; + + UniquePtr<mozilla::a11y::EmbeddedObjCollector> mEmbeddedObjCollector; + union { + int32_t mIndexOfEmbeddedChild; + uint32_t mProxyInterfaces; + } mInt; + + friend class EmbeddedObjCollector; + + union + { + AccGroupInfo* groupInfo; + ProxyAccessible* proxy; + } mBits; + friend class AccGroupInfo; + +private: + Accessible() = delete; + Accessible(const Accessible&) = delete; + Accessible& operator =(const Accessible&) = delete; + +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Accessible, + NS_ACCESSIBLE_IMPL_IID) + + +/** + * Represent key binding associated with accessible (such as access key and + * global keyboard shortcuts). + */ +class KeyBinding +{ +public: + /** + * Modifier mask values. + */ + static const uint32_t kShift = 1; + static const uint32_t kControl = 2; + static const uint32_t kAlt = 4; + static const uint32_t kMeta = 8; + static const uint32_t kOS = 16; + + static uint32_t AccelModifier(); + + KeyBinding() : mKey(0), mModifierMask(0) {} + KeyBinding(uint32_t aKey, uint32_t aModifierMask) : + mKey(aKey), mModifierMask(aModifierMask) {} + + inline bool IsEmpty() const { return !mKey; } + inline uint32_t Key() const { return mKey; } + inline uint32_t ModifierMask() const { return mModifierMask; } + + enum Format { + ePlatformFormat, + eAtkFormat + }; + + /** + * Return formatted string for this key binding depending on the given format. + */ + inline void ToString(nsAString& aValue, + Format aFormat = ePlatformFormat) const + { + aValue.Truncate(); + AppendToString(aValue, aFormat); + } + inline void AppendToString(nsAString& aValue, + Format aFormat = ePlatformFormat) const + { + if (mKey) { + if (aFormat == ePlatformFormat) + ToPlatformFormat(aValue); + else + ToAtkFormat(aValue); + } + } + +private: + void ToPlatformFormat(nsAString& aValue) const; + void ToAtkFormat(nsAString& aValue) const; + + uint32_t mKey; + uint32_t mModifierMask; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/ApplicationAccessible.cpp b/accessible/generic/ApplicationAccessible.cpp new file mode 100644 index 0000000000..ae8ca27e31 --- /dev/null +++ b/accessible/generic/ApplicationAccessible.cpp @@ -0,0 +1,201 @@ +/* -*- 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 "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "Relation.h" +#include "Role.h" +#include "States.h" + +#include "nsIComponentManager.h" +#include "nsIDOMDocument.h" +#include "nsIWindowMediator.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Services.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"); +} + +NS_IMPL_ISUPPORTS_INHERITED0(ApplicationAccessible, Accessible) + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessible + +ENameValueFlag +ApplicationAccessible::Name(nsString& aName) +{ + aName.Truncate(); + + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::services::GetStringBundleService(); + + 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; + + nsXPIDLString appName; + rv = bundle->GetStringFromName(u"brandShortName", + getter_Copies(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) +{ + aDescription.Truncate(); +} + +void +ApplicationAccessible::Value(nsString& aValue) +{ + aValue.Truncate(); +} + +uint64_t +ApplicationAccessible::State() +{ + return IsDefunct() ? states::DEFUNCT : 0; +} + +already_AddRefed<nsIPersistentProperties> +ApplicationAccessible::NativeAttributes() +{ + return nullptr; +} + +GroupPos +ApplicationAccessible::GroupPosition() +{ + return GroupPos(); +} + +Accessible* +ApplicationAccessible::ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) +{ + return nullptr; +} + +Accessible* +ApplicationAccessible::FocusedChild() +{ + Accessible* focus = FocusMgr()->FocusedAccessible(); + if (focus && focus->Parent() == this) + return focus; + + return nullptr; +} + +Relation +ApplicationAccessible::RelationByType(RelationType aRelationType) +{ + return Relation(); +} + +nsIntRect +ApplicationAccessible::Bounds() const +{ + return nsIntRect(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible public methods + +void +ApplicationAccessible::Shutdown() +{ + mAppInfo = nullptr; +} + +void +ApplicationAccessible::ApplyARIAState(uint64_t* aState) const +{ +} + +role +ApplicationAccessible::NativeRole() +{ + return roles::APP_ROOT; +} + +uint64_t +ApplicationAccessible::NativeState() +{ + 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. + + nsCOMPtr<nsIWindowMediator> windowMediator = + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID); + + nsCOMPtr<nsISimpleEnumerator> windowEnumerator; + nsresult rv = windowMediator->GetEnumerator(nullptr, + getter_AddRefs(windowEnumerator)); + if (NS_FAILED(rv)) + return; + + bool hasMore = false; + windowEnumerator->HasMoreElements(&hasMore); + while (hasMore) { + nsCOMPtr<nsISupports> window; + windowEnumerator->GetNext(getter_AddRefs(window)); + nsCOMPtr<nsPIDOMWindowOuter> DOMWindow = do_QueryInterface(window); + if (DOMWindow) { + nsCOMPtr<nsIDocument> docNode = DOMWindow->GetDoc(); + if (docNode) { + GetAccService()->GetDocAccessible(docNode); // ensure creation + } + } + windowEnumerator->HasMoreElements(&hasMore); + } +} + +Accessible* +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..7609a86e25 --- /dev/null +++ b/accessible/generic/ApplicationAccessible.h @@ -0,0 +1,120 @@ +/* -*- 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 "nsIMutableArray.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_DECL_ISUPPORTS_INHERITED + + // Accessible + virtual void Shutdown() override; + virtual nsIntRect Bounds() const override; + virtual already_AddRefed<nsIPersistentProperties> NativeAttributes() override; + virtual GroupPos GroupPosition() override; + virtual ENameValueFlag Name(nsString& aName) override; + virtual void ApplyARIAState(uint64_t* aState) const override; + virtual void Description(nsString& aDescription) override; + virtual void Value(nsString& aValue) override; + virtual mozilla::a11y::role NativeRole() override; + virtual uint64_t State() override; + virtual uint64_t NativeState() override; + virtual Relation RelationByType(RelationType aType) override; + + virtual Accessible* ChildAtPoint(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() {} + + // Accessible + virtual Accessible* GetSiblingAtOffset(int32_t aOffset, + nsresult *aError = nullptr) const override; + +private: + nsCOMPtr<nsIXULAppInfo> mAppInfo; +}; + +inline ApplicationAccessible* +Accessible::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..bcb262a97d --- /dev/null +++ b/accessible/generic/BaseAccessibles.cpp @@ -0,0 +1,264 @@ +/* -*- 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 "Accessible-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; +} + +NS_IMPL_ISUPPORTS_INHERITED0(LeafAccessible, Accessible) + +//////////////////////////////////////////////////////////////////////////////// +// LeafAccessible: Accessible public + +Accessible* +LeafAccessible::ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) +{ + // Don't walk into leaf accessibles. + return this; +} + +bool +LeafAccessible::InsertChildAt(uint32_t aIndex, Accessible* aChild) +{ + NS_NOTREACHED("InsertChildAt called on leaf accessible!"); + return false; +} + +bool +LeafAccessible::RemoveChild(Accessible* aChild) +{ + NS_NOTREACHED("RemoveChild called on leaf accessible!"); + return false; +} + +bool +LeafAccessible::IsAcceptableChild(nsIContent* aEl) const +{ + // No children for leaf accessible. + return false; +} + + +//////////////////////////////////////////////////////////////////////////////// +// LinkableAccessible +//////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS_INHERITED0(LinkableAccessible, AccessibleWrap) + +//////////////////////////////////////////////////////////////////////////////// +// LinkableAccessible. nsIAccessible + +void +LinkableAccessible::TakeFocus() +{ + if (Accessible* actionAcc = ActionWalk()) { + actionAcc->TakeFocus(); + } else { + AccessibleWrap::TakeFocus(); + } +} + +uint64_t +LinkableAccessible::NativeLinkState() const +{ + bool isLink; + Accessible* actionAcc = + const_cast<LinkableAccessible*>(this)->ActionWalk(&isLink); + if (isLink) { + return states::LINKED | (actionAcc->LinkState() & states::TRAVERSED); + } + + return 0; +} + +void +LinkableAccessible::Value(nsString& aValue) +{ + aValue.Truncate(); + + Accessible::Value(aValue); + if (!aValue.IsEmpty()) { + return; + } + + bool isLink; + Accessible* actionAcc = ActionWalk(&isLink); + if (isLink) { + actionAcc->Value(aValue); + } +} + +uint8_t +LinkableAccessible::ActionCount() +{ + bool isLink, isOnclick, isLabelWithControl; + ActionWalk(&isLink, &isOnclick, &isLabelWithControl); + return (isLink || isOnclick || isLabelWithControl) ? 1 : 0; +} + +Accessible* +LinkableAccessible::ActionWalk(bool* aIsLink, bool* aIsOnclick, + bool* aIsLabelWithControl) +{ + if (aIsOnclick) { + *aIsOnclick = false; + } + if (aIsLink) { + *aIsLink = false; + } + if (aIsLabelWithControl) { + *aIsLabelWithControl = false; + } + + if (nsCoreUtils::HasClickListener(mContent)) { + if (aIsOnclick) { + *aIsOnclick = true; + } + return nullptr; + } + + // XXX: The logic looks broken since the click listener may be registered + // on non accessible node in parent chain but this node is skipped when tree + // is traversed. + Accessible* walkUpAcc = this; + while ((walkUpAcc = walkUpAcc->Parent()) && !walkUpAcc->IsDoc()) { + if (walkUpAcc->LinkState() & states::LINKED) { + if (aIsLink) { + *aIsLink = true; + } + return walkUpAcc; + } + + if (nsCoreUtils::HasClickListener(walkUpAcc->GetContent())) { + if (aIsOnclick) { + *aIsOnclick = true; + } + return walkUpAcc; + } + + if (nsCoreUtils::IsLabelWithControl(walkUpAcc->GetContent())) { + if (aIsLabelWithControl) { + *aIsLabelWithControl = true; + } + return walkUpAcc; + } + } + return nullptr; +} + +void +LinkableAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) +{ + aName.Truncate(); + + // Action 0 (default action): Jump to link + if (aIndex == eAction_Jump) { + bool isOnclick, isLink, isLabelWithControl; + ActionWalk(&isLink, &isOnclick, &isLabelWithControl); + if (isLink) { + aName.AssignLiteral("jump"); + } else if (isOnclick || isLabelWithControl) { + aName.AssignLiteral("click"); + } + } +} + +bool +LinkableAccessible::DoAction(uint8_t aIndex) +{ + if (aIndex != eAction_Jump) { + return false; + } + + if (Accessible* actionAcc = ActionWalk()) { + return actionAcc->DoAction(aIndex); + } + + return AccessibleWrap::DoAction(aIndex); +} + +KeyBinding +LinkableAccessible::AccessKey() const +{ + if (const Accessible* actionAcc = + const_cast<LinkableAccessible*>(this)->ActionWalk()) { + return actionAcc->AccessKey(); + } + + return Accessible::AccessKey(); +} + +//////////////////////////////////////////////////////////////////////////////// +// LinkableAccessible: HyperLinkAccessible + +already_AddRefed<nsIURI> +LinkableAccessible::AnchorURIAt(uint32_t aAnchorIndex) +{ + bool isLink; + Accessible* 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() +{ + 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..e4c71c423c --- /dev/null +++ b/accessible/generic/BaseAccessibles.h @@ -0,0 +1,132 @@ +/* -*- 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_DECL_ISUPPORTS_INHERITED + + // Accessible + virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) override; + virtual bool InsertChildAt(uint32_t aIndex, Accessible* aChild) override final; + virtual bool RemoveChild(Accessible* aChild) override 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 activate (click) + * the host accessible programmatically. + */ +class LinkableAccessible : public AccessibleWrap +{ +public: + enum { eAction_Jump = 0 }; + + LinkableAccessible(nsIContent* aContent, DocAccessible* aDoc) : + AccessibleWrap(aContent, aDoc) + { + } + + NS_DECL_ISUPPORTS_INHERITED + + // Accessible + virtual void Value(nsString& aValue) override; + virtual uint64_t NativeLinkState() const override; + virtual void TakeFocus() override; + + // ActionAccessible + virtual uint8_t ActionCount() override; + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + virtual bool DoAction(uint8_t index) override; + virtual KeyBinding AccessKey() const override; + + // ActionAccessible helpers + Accessible* ActionWalk(bool* aIsLink = nullptr, + bool* aIsOnclick = nullptr, + bool* aIsLabelWithControl = nullptr); + // HyperLinkAccessible + virtual already_AddRefed<nsIURI> AnchorURIAt(uint32_t aAnchorIndex) 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 Accessible::QueryInterface(aIID, aPtr); } + + // Accessible + virtual a11y::role NativeRole() override { return R; } + +protected: + virtual ~EnumRoleAccessible() { } +}; + + +/** + * 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) { } + + virtual uint64_t NativeState() override final; + virtual uint64_t NativeInteractiveState() const override final; + virtual uint64_t NativeLinkState() const override final; + virtual bool NativelyUnavailable() const override final; + virtual void ApplyARIAState(uint64_t* aState) const override 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..af660ff47f --- /dev/null +++ b/accessible/generic/DocAccessible-inl.h @@ -0,0 +1,189 @@ +/* -*- 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 "nsIDocumentInlines.h" + +#ifdef A11Y_LOG +#include "Logging.h" +#endif + +namespace mozilla { +namespace a11y { + +inline Accessible* +DocAccessible::AccessibleOrTrueContainer(nsINode* aNode) const +{ + // HTML comboboxes have no-content list accessible as an intermediate + // containing all options. + Accessible* container = GetAccessibleOrContainer(aNode); + if (container && container->IsHTMLCombobox()) { + return container->FirstChild(); + } + return container; +} + +inline nsIAccessiblePivot* +DocAccessible::VirtualCursor() +{ + if (!mVirtualCursor) { + mVirtualCursor = new nsAccessiblePivot(this); + mVirtualCursor->AddObserver(this); + } + return mVirtualCursor; +} + +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, Accessible* aTarget) +{ + RefPtr<AccEvent> event = new AccEvent(aEventType, aTarget); + FireDelayedEvent(event); +} + +inline void +DocAccessible::BindChildDocument(DocAccessible* aDocument) +{ + mNotificationController->ScheduleChildDocBinding(aDocument); +} + +template<class Class, class Arg> +inline void +DocAccessible::HandleNotification(Class* aInstance, + typename TNotification<Class, Arg>::Callback aMethod, + Arg* aArg) +{ + if (mNotificationController) { + mNotificationController->HandleNotification<Class, Arg>(aInstance, + aMethod, aArg); + } +} + +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::AddScrollListener() +{ + // Delay scroll initializing until the document has a root frame. + if (!mPresShell->GetRootFrame()) + return; + + mDocFlags |= eScrollInitialized; + nsIScrollableFrame* sf = mPresShell->GetRootScrollFrameAsScrollable(); + if (sf) { + sf->AddScrollPositionListener(this); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) + logging::Text("add scroll listener"); +#endif + } +} + +inline void +DocAccessible::RemoveScrollListener() +{ + nsIScrollableFrame* sf = mPresShell->GetRootScrollFrameAsScrollable(); + if (sf) + sf->RemoveScrollPositionListener(this); +} + +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(Accessible* aAccessible) +{ + a11y::role role = aAccessible->Role(); + if (role == roles::ENTRY || role == roles::COMBOBOX) + FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, aAccessible); +} + +inline Accessible* +DocAccessible::GetAccessibleEvenIfNotInMapOrContainer(nsINode* aNode) const +{ + Accessible* acc = GetAccessibleEvenIfNotInMap(aNode); + return acc ? acc : GetContainerAccessible(aNode); +} + +inline void +DocAccessible::CreateSubtree(Accessible* 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. + Accessible* 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()); + } +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp new file mode 100644 index 0000000000..c89aa189b6 --- /dev/null +++ b/accessible/generic/DocAccessible.cpp @@ -0,0 +1,2387 @@ +/* -*- 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 "Accessible-inl.h" +#include "AccIterator.h" +#include "DocAccessible-inl.h" +#include "DocAccessibleChild.h" +#include "HTMLImageMapAccessible.h" +#include "nsAccCache.h" +#include "nsAccessiblePivot.h" +#include "nsAccUtils.h" +#include "nsEventShell.h" +#include "nsTextEquivUtils.h" +#include "Role.h" +#include "RootAccessible.h" +#include "TreeWalker.h" +#include "xpcAccessibleDocument.h" + +#include "nsIMutableArray.h" +#include "nsICommandManager.h" +#include "nsIDocShell.h" +#include "nsIDocument.h" +#include "nsIDOMAttr.h" +#include "nsIDOMCharacterData.h" +#include "nsIDOMDocument.h" +#include "nsIDOMXULDocument.h" +#include "nsIDOMMutationEvent.h" +#include "nsPIDOMWindow.h" +#include "nsIDOMXULPopupElement.h" +#include "nsIEditingSession.h" +#include "nsIFrame.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsImageFrame.h" +#include "nsIPersistentProperties2.h" +#include "nsIPresShell.h" +#include "nsIServiceManager.h" +#include "nsViewManager.h" +#include "nsIScrollableFrame.h" +#include "nsUnicharUtils.h" +#include "nsIURI.h" +#include "nsIWebNavigation.h" +#include "nsFocusManager.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/EventStates.h" +#include "mozilla/dom/DocumentType.h" +#include "mozilla/dom/Element.h" + +#ifdef MOZ_XUL +#include "nsIXULDocument.h" +#endif + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Static member initialization + +static nsIAtom** 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(nsIDocument* aDocument, nsIPresShell* aPresShell) : + // XXX don't pass a document to the Accessible 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 Accessible::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), + mScrollPositionChangedTicks(0), + mLoadState(eTreeConstructionPending), mDocFlags(0), mLoadEventType(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); + + // If this is a XUL Document, it should not implement nsHyperText + if (mDocumentNode && mDocumentNode->IsXULDocument()) + mGenericTypes &= ~eHyperText; +} + +DocAccessible::~DocAccessible() +{ + NS_ASSERTION(!mPresShell, "LastRelease was never called!?!"); +} + + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, Accessible) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVirtualCursor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments) + for (auto iter = tmp->mDependentIDsHash.Iter(); !iter.Done(); iter.Next()) { + AttrRelProviderArray* providers = iter.UserData(); + + for (int32_t jdx = providers->Length() - 1; jdx >= 0; jdx--) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( + cb, "content of dependent ids hash entry of document accessible"); + + AttrRelProvider* provider = (*providers)[jdx]; + cb.NoteXPCOMChild(provider->mContent); + + NS_ASSERTION(provider->mContent->IsInUncomposedDoc(), + "Referred content is not in document!"); + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList) + for (auto it = tmp->mARIAOwnsHash.ConstIter(); !it.Done(); it.Next()) { + nsTArray<RefPtr<Accessible> >* ar = it.UserData(); + 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, Accessible) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mVirtualCursor) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments) + tmp->mDependentIDsHash.Clear(); + tmp->mNodeToAccessibleMap.Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList) + tmp->mARIAOwnsHash.Clear(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DocAccessible) + NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + 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) +{ + 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 + Accessible::Name(aName); + } + if (aName.IsEmpty()) { + Title(aName); // Try title element + } + if (aName.IsEmpty()) { // Last resort: use URL + URL(aName); + } + + return eNameOK; +} + +// Accessible public method +role +DocAccessible::NativeRole() +{ + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode); + if (docShell) { + nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot; + docShell->GetSameTypeRootTreeItem(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) { +#ifdef MOZ_XUL + nsCOMPtr<nsIXULDocument> xulDoc(do_QueryInterface(mDocumentNode)); + if (xulDoc) + return roles::APPLICATION; +#endif + return roles::DOCUMENT; + } + } + else if (itemType == nsIDocShellTreeItem::typeContent) { + return roles::DOCUMENT; + } + } + + return roles::PANE; // Fall back; +} + +void +DocAccessible::Description(nsString& aDescription) +{ + if (mParent) + mParent->Description(aDescription); + + if (HasOwnContent() && aDescription.IsEmpty()) { + nsTextEquivUtils:: + GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, + aDescription); + } +} + +// Accessible public method +uint64_t +DocAccessible::NativeState() +{ + // 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; + } + + nsCOMPtr<nsIEditor> editor = GetEditor(); + state |= editor ? states::EDITABLE : states::READONLY; + + return state; +} + +uint64_t +DocAccessible::NativeInteractiveState() const +{ + // Document is always focusable. + return states::FOCUSABLE; +} + +bool +DocAccessible::NativelyUnavailable() const +{ + return false; +} + +// Accessible public method +void +DocAccessible::ApplyARIAState(uint64_t* aState) const +{ + // Grab states from content element. + if (mContent) + Accessible::ApplyARIAState(aState); + + // Allow iframe/frame etc. to have final state override via ARIA. + if (mParent) + mParent->ApplyARIAState(aState); +} + +already_AddRefed<nsIPersistentProperties> +DocAccessible::Attributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = + HyperTextAccessibleWrap::Attributes(); + + // No attributes if document is not attached to the tree or if it's a root + // document. + if (!mParent || IsRoot()) + return attributes.forget(); + + // Override ARIA object attributes from outerdoc. + aria::AttrIterator attribIter(mParent->GetContent()); + nsAutoString name, value, unused; + while(attribIter.Next(name, value)) + attributes->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused); + + return attributes.forget(); +} + +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() +{ + // Focus the document. + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + nsCOMPtr<nsIDOMElement> newFocus; + fm->MoveFocus(mDocumentNode->GetWindow(), nullptr, + nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus)); +} + +// HyperTextAccessible method +already_AddRefed<nsIEditor> +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->HasFlag(NODE_IS_EDITABLE) && + (!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 + + nsCOMPtr<nsIEditor> editor; + editingSession->GetEditorForWindow(mDocumentNode->GetWindow(), getter_AddRefs(editor)); + if (!editor) + return nullptr; + + bool isEditable = false; + editor->GetIsDocumentEditable(&isEditable); + if (isEditable) + return editor.forget(); + + return nullptr; +} + +// DocAccessible public method + +void +DocAccessible::URL(nsAString& aURL) const +{ + nsCOMPtr<nsISupports> container = mDocumentNode->GetContainer(); + nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container)); + nsAutoCString theURL; + if (webNav) { + nsCOMPtr<nsIURI> pURI; + webNav->GetCurrentURI(getter_AddRefs(pURI)); + if (pURI) + pURI->GetSpec(theURL); + } + CopyUTF8toUTF16(theURL, aURL); +} + +void +DocAccessible::DocType(nsAString& aType) const +{ +#ifdef MOZ_XUL + nsCOMPtr<nsIXULDocument> xulDoc(do_QueryInterface(mDocumentNode)); + if (xulDoc) { + aType.AssignLiteral("window"); // doctype not implemented for XUL at time of writing - causes assertion + return; + } +#endif + dom::DocumentType* docType = mDocumentNode->GetDoctype(); + if (docType) + docType->GetPublicId(aType); +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible + +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 document accessible as loaded if its DOM document was loaded at + // this point (this can happen because a11y is started late or DOM document + // having no container was loaded. + if (mDocumentNode->GetReadyStateEnum() == nsIDocument::READYSTATE_COMPLETE) + 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(); + + nsCOMPtr<nsIDocument> kungFuDeathGripDoc = mDocumentNode; + mDocumentNode = nullptr; + + if (mParent) { + DocAccessible* parentDocument = mParent->Document(); + if (parentDocument) + parentDocument->RemoveChildDocument(this); + + mParent->RemoveChild(this); + } + + // 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(); + + // XXX thinking about ordering? + if (mIPCDoc) { + MOZ_ASSERT(IPCAccessibilityActive()); + mIPCDoc->Shutdown(); + MOZ_ASSERT(!mIPCDoc); + } + + if (mVirtualCursor) { + mVirtualCursor->RemoveObserver(this); + mVirtualCursor = nullptr; + } + + mPresShell->SetDocAccessible(nullptr); + mPresShell = nullptr; // Avoid reentrancy + + mDependentIDsHash.Clear(); + mNodeToAccessibleMap.Clear(); + + for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) { + Accessible* accessible = iter.Data(); + MOZ_ASSERT(accessible); + if (accessible && !accessible->IsDefunct()) { + // Unlink parent to avoid its cleaning overhead in shutdown. + accessible->mParent = nullptr; + accessible->Shutdown(); + } + iter.Remove(); + } + + HyperTextAccessibleWrap::Shutdown(); + + GetAccService()->NotifyOfDocumentShutdown(this, kungFuDeathGripDoc); +} + +nsIFrame* +DocAccessible::GetFrame() const +{ + nsIFrame* root = nullptr; + if (mPresShell) + root = mPresShell->GetRootFrame(); + + return root; +} + +// DocAccessible protected member +nsRect +DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const +{ + *aRelativeFrame = GetFrame(); + + nsIDocument *document = mDocumentNode; + nsIDocument *parentDoc = nullptr; + + nsRect bounds; + while (document) { + nsIPresShell *presShell = document->GetShell(); + if (!presShell) + return nsRect(); + + nsRect scrollPort; + nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollableExternal(); + 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->GetParentDocument(); + } + + return bounds; +} + +// DocAccessible protected member +nsresult +DocAccessible::AddEventListeners() +{ + nsCOMPtr<nsIDocShell> docShell(mDocumentNode->GetDocShell()); + + // We want to add a command observer only if the document is content and has + // an editor. + if (docShell->ItemType() == nsIDocShellTreeItem::typeContent) { + nsCOMPtr<nsICommandManager> commandManager = docShell->GetCommandManager(); + if (commandManager) + commandManager->AddCommandObserver(this, "obs_documentCreated"); + } + + SelectionMgr()->AddDocSelectionListener(mPresShell); + + // Add document observer. + mDocumentNode->AddObserver(this); + return NS_OK; +} + +// DocAccessible protected member +nsresult +DocAccessible::RemoveEventListeners() +{ + // Remove listeners associated with content documents + // Remove scroll position listener + RemoveScrollListener(); + + NS_ASSERTION(mDocumentNode, "No document during removal of listeners."); + + if (mDocumentNode) { + mDocumentNode->RemoveObserver(this); + + nsCOMPtr<nsIDocShell> docShell(mDocumentNode->GetDocShell()); + NS_ASSERTION(docShell, "doc should support nsIDocShellTreeItem."); + + if (docShell) { + if (docShell->ItemType() == nsIDocShellTreeItem::typeContent) { + nsCOMPtr<nsICommandManager> commandManager = docShell->GetCommandManager(); + if (commandManager) { + commandManager->RemoveCommandObserver(this, "obs_documentCreated"); + } + } + } + } + + 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 && docAcc->mScrollPositionChangedTicks && + ++docAcc->mScrollPositionChangedTicks > 2) { + // Whenever scroll position changes, mScrollPositionChangeTicks gets reset to 1 + // We only want to fire accessibilty scroll event when scrolling stops or pauses + // Therefore, we wait for no scroll events to occur between 2 ticks of this timer + // That indicates a pause in scrolling, so we fire the accessibilty scroll event + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_END, docAcc); + + docAcc->mScrollPositionChangedTicks = 0; + if (docAcc->mScrollWatchTimer) { + docAcc->mScrollWatchTimer->Cancel(); + docAcc->mScrollWatchTimer = nullptr; + NS_RELEASE(docAcc); // Release kung fu death grip + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIScrollPositionListener + +void +DocAccessible::ScrollPositionDidChange(nscoord aX, nscoord aY) +{ + // Start new timer, if the timer cycles at least 1 full cycle without more scroll position changes, + // then the ::Notify() method will fire the accessibility event for scroll position changes + const uint32_t kScrollPosCheckWait = 50; + if (mScrollWatchTimer) { + mScrollWatchTimer->SetDelay(kScrollPosCheckWait); // Create new timer, to avoid leaks + } + else { + mScrollWatchTimer = do_CreateInstance("@mozilla.org/timer;1"); + if (mScrollWatchTimer) { + NS_ADDREF_THIS(); // Kung fu death grip + mScrollWatchTimer->InitWithFuncCallback(ScrollTimerCallback, this, + kScrollPosCheckWait, + nsITimer::TYPE_REPEATING_SLACK); + } + } + mScrollPositionChangedTicks = 1; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIObserver + +NS_IMETHODIMP +DocAccessible::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + if (!nsCRT::strcmp(aTopic,"obs_documentCreated")) { + // State editable will now be set, readonly is now clear + // Normally we only fire delayed events created from the node, not an + // accessible object. See the AccStateChangeEvent constructor for details + // about this exceptional case. + RefPtr<AccEvent> event = + new AccStateChangeEvent(this, states::EDITABLE, true); + FireDelayedEvent(event); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessiblePivotObserver + +NS_IMETHODIMP +DocAccessible::OnPivotChanged(nsIAccessiblePivot* aPivot, + nsIAccessible* aOldAccessible, + int32_t aOldStart, int32_t aOldEnd, + PivotMoveReason aReason, + bool aIsFromUserInput) +{ + RefPtr<AccEvent> event = + new AccVCChangeEvent( + this, (aOldAccessible ? aOldAccessible->ToInternalAccessible() : nullptr), + aOldStart, aOldEnd, aReason, + aIsFromUserInput ? eFromUserInput : eNoUserInput); + nsEventShell::FireEvent(event); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIDocumentObserver + +NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible) +NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible) +NS_IMPL_NSIDOCUMENTOBSERVER_STYLE_STUB(DocAccessible) + +void +DocAccessible::AttributeWillChange(nsIDocument* aDocument, + dom::Element* aElement, + int32_t aNameSpaceID, + nsIAtom* aAttribute, int32_t aModType, + const nsAttrValue* aNewValue) +{ + Accessible* 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 != nsIDOMMutationEvent::ADDITION) + RemoveDependentIDsFor(accessible, aAttribute); + + if (aAttribute == nsGkAtoms::id) { + RelocateARIAOwnedIfNeeded(aElement); + } + + // Store the ARIA attribute old value so that it can be used after + // attribute change. Note, we assume there's no nested ARIA attribute + // changes. If this happens then we should end up with keeping a stack of + // old values. + + // XXX TODO: bugs 472142, 472143. + // Here we will want to cache whatever attribute values we are interested + // in, such as the existence of aria-pressed for button (so we know if we + // need to newly expose it as a toggle button) etc. + if (aAttribute == nsGkAtoms::aria_checked || + aAttribute == nsGkAtoms::aria_pressed) { + mARIAAttrOldValue = (aModType != nsIDOMMutationEvent::ADDITION) ? + nsAccUtils::GetARIAToken(aElement, aAttribute) : nullptr; + return; + } + + if (aAttribute == nsGkAtoms::aria_disabled || + aAttribute == nsGkAtoms::disabled) + mStateBitWasOn = accessible->Unavailable(); +} + +void +DocAccessible::NativeAnonymousChildListChange(nsIDocument* aDocument, + nsIContent* aContent, + bool aIsRemove) +{ +} + +void +DocAccessible::AttributeChanged(nsIDocument* aDocument, + dom::Element* aElement, + int32_t aNameSpaceID, nsIAtom* 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; + + // Ignore attribute change if the element doesn't have an accessible (at all + // or still) iff the element is not a root content of this document accessible + // (which is treated as attribute change on this document accessible). + // Note: we don't bail if all the content hasn't finished loading because + // these attributes are changing for a loaded part of the content. + Accessible* accessible = GetAccessible(aElement); + if (!accessible) { + if (mContent != aElement) + return; + + accessible = this; + } + + MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(), + "DOM attribute change on an accessible detached from the tree"); + + // Fire accessible events iff there's an accessible, otherwise we consider + // the accessible state wasn't changed, i.e. its state is initial state. + AttributeChangedImpl(accessible, aNameSpaceID, aAttribute); + + // Update dependent IDs cache. Take care of accessible elements because no + // accessible element means either the element is not accessible at all or + // its accessible will be created later. It doesn't make sense to keep + // dependent IDs for non accessible elements. For the second case we'll update + // dependent IDs cache when its accessible is created. + if (aModType == nsIDOMMutationEvent::MODIFICATION || + aModType == nsIDOMMutationEvent::ADDITION) { + AddDependentIDsFor(accessible, aAttribute); + } +} + +// DocAccessible protected member +void +DocAccessible::AttributeChangedImpl(Accessible* aAccessible, + int32_t aNameSpaceID, nsIAtom* aAttribute) +{ + // 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 + + // Universal boolean properties that don't require a role. Fire the state + // change when disabled or aria-disabled attribute is set. + // Note. Checking the XUL or HTML namespace would not seem to gain us + // anything, because disabled attribute really is going to mean the same + // thing in any namespace. + // Note. We use the attribute instead of the disabled state bit because + // ARIA's aria-disabled does not affect the disabled state bit. + if (aAttribute == nsGkAtoms::disabled || + aAttribute == nsGkAtoms::aria_disabled) { + // Do nothing if state wasn't changed (like @aria-disabled was removed but + // @disabled is still presented). + if (aAccessible->Unavailable() == mStateBitWasOn) + return; + + RefPtr<AccEvent> enabledChangeEvent = + new AccStateChangeEvent(aAccessible, states::ENABLED, mStateBitWasOn); + FireDelayedEvent(enabledChangeEvent); + + RefPtr<AccEvent> sensitiveChangeEvent = + new AccStateChangeEvent(aAccessible, states::SENSITIVE, mStateBitWasOn); + FireDelayedEvent(sensitiveChangeEvent); + return; + } + + // Check for namespaced ARIA attribute + if (aNameSpaceID == kNameSpaceID_None) { + // Check for hyphenated aria-foo property? + if (StringBeginsWith(nsDependentAtomString(aAttribute), + NS_LITERAL_STRING("aria-"))) { + ARIAAttributeChanged(aAccessible, aAttribute); + } + } + + // Fire name change and description change events. XXX: it's not complete and + // dupes the code logic of accessible name and description calculation, we do + // that for performance reasons. + if (aAttribute == nsGkAtoms::aria_label) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible); + return; + } + + if (aAttribute == nsGkAtoms::aria_describedby) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, aAccessible); + return; + } + + nsIContent* elm = aAccessible->GetContent(); + if (aAttribute == nsGkAtoms::aria_labelledby && + !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label)) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible); + return; + } + + if (aAttribute == nsGkAtoms::alt && + !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) && + !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby)) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible); + return; + } + + if (aAttribute == nsGkAtoms::title) { + if (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) && + !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby) && + !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::alt)) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible); + return; + } + + if (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_describedby)) + FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, aAccessible); + + return; + } + + if (aAttribute == nsGkAtoms::aria_busy) { + bool isOn = elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true, + eCaseMatters); + RefPtr<AccEvent> event = + new AccStateChangeEvent(aAccessible, states::BUSY, isOn); + FireDelayedEvent(event); + return; + } + + if (aAttribute == nsGkAtoms::id) { + RelocateARIAOwnedIfNeeded(elm); + } + + // ARIA or XUL selection + if ((aAccessible->GetContent()->IsXULElement() && + aAttribute == nsGkAtoms::selected) || + aAttribute == nsGkAtoms::aria_selected) { + Accessible* widget = + nsAccUtils::GetSelectableContainer(aAccessible, aAccessible->State()); + if (widget) { + AccSelChangeEvent::SelChangeType selChangeType = + elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true, eCaseMatters) ? + AccSelChangeEvent::eSelectionAdd : AccSelChangeEvent::eSelectionRemove; + + RefPtr<AccEvent> event = + new AccSelChangeEvent(widget, aAccessible, selChangeType); + FireDelayedEvent(event); + } + + return; + } + + if (aAttribute == nsGkAtoms::contenteditable) { + RefPtr<AccEvent> editableChangeEvent = + new AccStateChangeEvent(aAccessible, states::EDITABLE); + FireDelayedEvent(editableChangeEvent); + return; + } + + if (aAttribute == nsGkAtoms::value) { + if (aAccessible->IsProgress()) + FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible); + } +} + +// DocAccessible protected member +void +DocAccessible::ARIAAttributeChanged(Accessible* aAccessible, nsIAtom* aAttribute) +{ + // Note: For universal/global ARIA states and properties we don't care if + // there is an ARIA role present or not. + + if (aAttribute == nsGkAtoms::aria_required) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(aAccessible, states::REQUIRED); + FireDelayedEvent(event); + return; + } + + if (aAttribute == nsGkAtoms::aria_invalid) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(aAccessible, states::INVALID); + FireDelayedEvent(event); + return; + } + + // 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. + if (aAttribute == nsGkAtoms::aria_activedescendant) { + mNotificationController->HandleNotification<DocAccessible, Accessible> + (this, &DocAccessible::ARIAActiveDescendantChanged, aAccessible); + + return; + } + + // We treat aria-expanded as a global ARIA state for historical reasons + if (aAttribute == nsGkAtoms::aria_expanded) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(aAccessible, states::EXPANDED); + FireDelayedEvent(event); + return; + } + + // 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. + uint8_t attrFlags = aria::AttrCharacteristicsFor(aAttribute); + if (!(attrFlags & ATTR_BYPASSOBJ)) { + RefPtr<AccEvent> event = + new AccObjectAttrChangedEvent(aAccessible, aAttribute); + FireDelayedEvent(event); + } + + nsIContent* elm = aAccessible->GetContent(); + + // Update aria-hidden flag for the whole subtree iff aria-hidden is changed + // on the root, i.e. ignore any affiliated aria-hidden changes in the subtree + // of top aria-hidden. + if (aAttribute == nsGkAtoms::aria_hidden) { + bool isDefined = aria::HasDefinedARIAHidden(elm); + if (isDefined != aAccessible->IsARIAHidden() && + (!aAccessible->Parent() || !aAccessible->Parent()->IsARIAHidden())) { + aAccessible->SetARIAHidden(isDefined); + + RefPtr<AccEvent> event = + new AccObjectAttrChangedEvent(aAccessible, aAttribute); + FireDelayedEvent(event); + } + return; + } + + if (aAttribute == nsGkAtoms::aria_checked || + (aAccessible->IsButton() && + aAttribute == nsGkAtoms::aria_pressed)) { + const uint64_t kState = (aAttribute == nsGkAtoms::aria_checked) ? + states::CHECKED : states::PRESSED; + RefPtr<AccEvent> event = new AccStateChangeEvent(aAccessible, kState); + FireDelayedEvent(event); + + bool wasMixed = (mARIAAttrOldValue == nsGkAtoms::mixed); + bool isMixed = elm->AttrValueIs(kNameSpaceID_None, aAttribute, + nsGkAtoms::mixed, eCaseMatters); + if (isMixed != wasMixed) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(aAccessible, states::MIXED, isMixed); + FireDelayedEvent(event); + } + return; + } + + if (aAttribute == nsGkAtoms::aria_readonly) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(aAccessible, states::READONLY); + FireDelayedEvent(event); + return; + } + + // Fire text value change event whenever aria-valuetext is changed. + if (aAttribute == nsGkAtoms::aria_valuetext) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, aAccessible); + return; + } + + // Fire numeric value change event when aria-valuenow is changed and + // aria-valuetext is empty + if (aAttribute == nsGkAtoms::aria_valuenow && + (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuetext) || + elm->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_valuetext, + nsGkAtoms::_empty, eCaseMatters))) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible); + return; + } + + if (aAttribute == nsGkAtoms::aria_owns) { + mNotificationController->ScheduleRelocation(aAccessible); + } +} + +void +DocAccessible::ARIAActiveDescendantChanged(Accessible* aAccessible) +{ + nsIContent* elm = aAccessible->GetContent(); + if (elm && aAccessible->IsActiveWidget()) { + nsAutoString id; + if (elm->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_activedescendant, id)) { + dom::Element* activeDescendantElm = elm->OwnerDoc()->GetElementById(id); + if (activeDescendantElm) { + Accessible* activeDescendant = GetAccessible(activeDescendantElm); + if (activeDescendant) { + FocusMgr()->ActiveItemChanged(activeDescendant, false); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::ActiveItemChangeCausedBy("ARIA activedescedant changed", + activeDescendant); +#endif + } + } + } + } +} + +void +DocAccessible::ContentAppended(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aFirstNewContent, + int32_t /* unused */) +{ +} + +void +DocAccessible::ContentStateChanged(nsIDocument* aDocument, + nsIContent* aContent, + EventStates aStateMask) +{ + Accessible* accessible = GetAccessible(aContent); + if (!accessible) + return; + + if (aStateMask.HasState(NS_EVENT_STATE_CHECKED)) { + Accessible* widget = accessible->ContainerWidget(); + if (widget && widget->IsSelect()) { + AccSelChangeEvent::SelChangeType selChangeType = + aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED) ? + AccSelChangeEvent::eSelectionAdd : AccSelChangeEvent::eSelectionRemove; + RefPtr<AccEvent> event = + new AccSelChangeEvent(widget, accessible, selChangeType); + FireDelayedEvent(event); + return; + } + + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::CHECKED, + aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED)); + FireDelayedEvent(event); + } + + if (aStateMask.HasState(NS_EVENT_STATE_INVALID)) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::INVALID, true); + FireDelayedEvent(event); + } + + if (aStateMask.HasState(NS_EVENT_STATE_VISITED)) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::TRAVERSED, true); + FireDelayedEvent(event); + } +} + +void +DocAccessible::DocumentStatesChanged(nsIDocument* aDocument, + EventStates aStateMask) +{ +} + +void +DocAccessible::CharacterDataWillChange(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ +} + +void +DocAccessible::CharacterDataChanged(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ +} + +void +DocAccessible::ContentInserted(nsIDocument* aDocument, nsIContent* aContainer, + nsIContent* aChild, int32_t /* unused */) +{ +} + +void +DocAccessible::ContentRemoved(nsIDocument* aDocument, + nsIContent* aContainerNode, + nsIContent* aChildNode, int32_t /* unused */, + nsIContent* aPreviousSiblingNode) +{ +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "DOM content removed; doc: %p", this); + logging::Node("container node", aContainerNode); + 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. + Accessible* container = GetAccessibleOrContainer(aContainerNode); + if (container) { + UpdateTreeOnRemoval(container, aChildNode); + } +} + +void +DocAccessible::ParentChainChanged(nsIContent* aContent) +{ +} + + +//////////////////////////////////////////////////////////////////////////////// +// Accessible + +#ifdef A11Y_LOG +nsresult +DocAccessible::HandleAccEvent(AccEvent* aEvent) +{ + if (logging::IsEnabled(logging::eDocLoad)) + logging::DocLoadEventHandled(aEvent); + + return HyperTextAccessible::HandleAccEvent(aEvent); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Public members + +void* +DocAccessible::GetNativeWindow() const +{ + if (!mPresShell) + return nullptr; + + nsViewManager* vm = mPresShell->GetViewManager(); + if (!vm) + return nullptr; + + nsCOMPtr<nsIWidget> widget; + vm->GetRootWidget(getter_AddRefs(widget)); + if (widget) + return widget->GetNativeData(NS_NATIVE_WINDOW); + + return nullptr; +} + +Accessible* +DocAccessible::GetAccessibleByUniqueIDInSubtree(void* aUniqueID) +{ + Accessible* 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; +} + +Accessible* +DocAccessible::GetAccessibleOrContainer(nsINode* aNode) const +{ + if (!aNode || !aNode->GetComposedDoc()) + return nullptr; + + nsINode* currNode = aNode; + Accessible* accessible = nullptr; + while (!(accessible = GetAccessible(currNode))) { + nsINode* parent = nullptr; + + // If this is a content node, try to get a flattened parent content node. + // This will smartly skip from the shadow root to the host element, + // over parentless document fragment + if (currNode->IsContent()) + parent = currNode->AsContent()->GetFlattenedTreeParent(); + + // Fallback to just get parent node, in case there is no parent content + // node. Or current node is not a content node. + if (!parent) + parent = currNode->GetParentNode(); + + if (!(currNode = parent)) break; + } + + return accessible; +} + +Accessible* +DocAccessible::GetAccessibleOrDescendant(nsINode* aNode) const +{ + Accessible* acc = GetAccessible(aNode); + if (acc) + return acc; + + acc = GetContainerAccessible(aNode); + if (acc) { + uint32_t childCnt = acc->ChildCount(); + for (uint32_t idx = 0; idx < childCnt; idx++) { + Accessible* child = acc->GetChildAt(idx); + for (nsIContent* elm = child->GetContent(); + elm && elm != acc->GetContent(); + elm = elm->GetFlattenedTreeParent()) { + if (elm == aNode) + return child; + } + } + } + + return nullptr; +} + +void +DocAccessible::BindToDocument(Accessible* aAccessible, + const nsRoleMapEntry* aRoleMapEntry) +{ + // Put into DOM node cache. + if (aAccessible->IsNodeMapEntry()) + mNodeToAccessibleMap.Put(aAccessible->GetNode(), aAccessible); + + // Put into unique ID cache. + mAccessibleCache.Put(aAccessible->UniqueID(), aAccessible); + + aAccessible->SetRoleMapEntry(aRoleMapEntry); + + AddDependentIDsFor(aAccessible); + + if (aAccessible->HasOwnContent()) { + nsIContent* el = aAccessible->GetContent(); + if (el->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_owns)) { + mNotificationController->ScheduleRelocation(aAccessible); + } + } +} + +void +DocAccessible::UnbindFromDocument(Accessible* aAccessible) +{ + NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()), + "Unbinding the unbound accessible!"); + + // Fire focus event on accessible having DOM focus if active item was removed + // from the tree. + if (FocusMgr()->IsActiveItem(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* aContainerNode, + 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)) { + // Update the whole tree of this document accessible when the container is + // null (document element is inserted or removed). + Accessible* container = aContainerNode ? + AccessibleOrTrueContainer(aContainerNode) : this; + if (container) { + // Ignore notification if the container node is no longer in the DOM tree. + mNotificationController->ScheduleContentInsertion(container, + aStartChildNode, + aEndChildNode); + } + } +} + +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. + + nsIContent* parent = aContent->GetFlattenedTreeParent(); + ContentRemoved(parent, aContent); + ContentInserted(parent, 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()) { + Accessible* 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. + AttrRelProviderArray* list = + mDependentIDsHash.Get(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(); +} + +Accessible* +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) { + Accessible* parent = GetAccessible(imageFrame->GetContent()); + if (parent) { + Accessible* 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) { + // 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() +{ + if (nsCoreUtils::IsTabDocument(mDocumentNode)) + mDocFlags |= eTabDocument; + + 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); + } +#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(Parent()); + ParentDocument()->FireDelayedEvent(reorderEvent); + } + + TreeMutation mt(this); + uint32_t childCount = ChildCount(); + for (uint32_t i = 0; i < childCount; i++) { + Accessible* child = GetChildAt(i); + mt.AfterInsertion(child); + } + mt.Done(); +} + +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(Accessible* aRelProvider, nsIAtom* aRelAttr) +{ + dom::Element* relProviderEl = aRelProvider->Elm(); + if (!relProviderEl) + return; + + for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) { + nsIAtom* 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; + + AttrRelProviderArray* providers = mDependentIDsHash.Get(id); + if (!providers) { + providers = new AttrRelProviderArray(); + if (providers) { + mDependentIDsHash.Put(id, providers); + } + } + + 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(Accessible* aRelProvider, + nsIAtom* aRelAttr) +{ + dom::Element* relProviderElm = aRelProvider->Elm(); + if (!relProviderElm) + return; + + for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) { + nsIAtom* 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; + + AttrRelProviderArray* providers = mDependentIDsHash.Get(id); + if (providers) { + for (uint32_t jdx = 0; jdx < providers->Length(); ) { + AttrRelProvider* provider = (*providers)[jdx]; + if (provider->mRelAttr == relAttr && + provider->mContent == relProviderElm) + providers->RemoveElement(provider); + else + jdx++; + } + if (providers->Length() == 0) + mDependentIDsHash.Remove(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, + nsIAtom* 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) { + SetRoleMapEntry(aria::GetRoleMap(aElement)); + if (mIPCDoc) { + mIPCDoc->SendRoleChangedEvent(Role()); + } + + 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::href) { + // Not worth the expense to ensure which namespace these are in. It doesn't + // kill use to recreate the accessible even if the attribute was used in + // the wrong namespace or an element that doesn't support it. + + // Make sure the accessible is recreated asynchronously to allow the content + // to handle the attribute change. + RecreateAccessible(aElement); + return true; + } + + if (aAttribute == nsGkAtoms::aria_multiselectable && + aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::role)) { + // This affects whether the accessible supports SelectAccessible. + // COM says we cannot change what interfaces are supported on-the-fly, + // so invalidate this object. A new one will be created on demand. + RecreateAccessible(aElement); + + return true; + } + + return false; +} + +void +DocAccessible::UpdateRootElIfNeeded() +{ + dom::Element* rootEl = mDocumentNode->GetBodyElement(); + if (!rootEl) { + rootEl = mDocumentNode->GetRootElement(); + } + if (rootEl != mContent) { + mContent = rootEl; + SetRoleMapEntry(aria::GetRoleMap(rootEl)); + if (mIPCDoc) { + mIPCDoc->SendRoleChangedEvent(Role()); + } + } +} + +/** + * Content insertion helper. + */ +class InsertIterator final +{ +public: + InsertIterator(Accessible* 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); + } + ~InsertIterator() { MOZ_COUNT_DTOR(InsertIterator); } + + Accessible* Context() const { return mWalker.Context(); } + Accessible* Child() const { return mChild; } + Accessible* 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: + Accessible* mChild; + Accessible* mChildBefore; + TreeWalker mWalker; + + const nsTArray<nsCOMPtr<nsIContent> >* mNodes; + uint32_t mNodesIdx; +}; + +bool +InsertIterator::Next() +{ + if (mNodesIdx > 0) { + Accessible* nextChild = mWalker.Next(); + if (nextChild) { + mChildBefore = mChild; + mChild = nextChild; + return true; + } + } + + while (mNodesIdx < mNodes->Length()) { + // 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. + nsIContent* prevNode = mNodes->SafeElementAt(mNodesIdx - 1); + nsIContent* node = mNodes->ElementAt(mNodesIdx++); + Accessible* container = Document()->AccessibleOrTrueContainer(node); + if (container != Context()) { + continue; + } + + // HTML comboboxes have no-content list accessible as an intermediate + // containing all options. + if (container->IsHTMLCombobox()) { + container = container->FirstChild(); + } + + if (!container->IsAcceptableChild(node)) { + continue; + } + +#ifdef A11Y_LOG + logging::TreeInfo("traversing an inserted node", logging::eVerbose, + "container", container, "node", node); +#endif + + // If inserted nodes are siblings then just move the walker next. + if (mChild && prevNode && prevNode->GetNextSibling() == node) { + Accessible* nextChild = mWalker.Scope(node); + if (nextChild) { + mChildBefore = mChild; + mChild = nextChild; + return true; + } + } + else { + TreeWalker finder(container); + if (finder.Seek(node)) { + mChild = mWalker.Scope(node); + if (mChild) { + mChildBefore = finder.Prev(); + return true; + } + } + } + } + + return false; +} + +void +DocAccessible::ProcessContentInserted(Accessible* 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 { + Accessible* parent = iter.Child()->Parent(); + if (parent) { + if (parent != aContainer) { +#ifdef A11Y_LOG + logging::TreeInfo("stealing accessible", 0, + "old parent", parent, "new parent", + aContainer, "child", iter.Child(), nullptr); +#endif + MOZ_ASSERT_UNREACHABLE("stealing accessible"); + continue; + } + +#ifdef A11Y_LOG + logging::TreeInfo("binding to same parent", logging::eVerbose, + "parent", aContainer, "child", iter.Child(), nullptr); +#endif + 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(Accessible* 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)) { + Accessible* 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(Accessible* aContainer) +{ + // Check to see if change occurred inside an alert, and fire an EVENT_ALERT + // if it did. + if (aContainer->IsAlert() || aContainer->IsInsideAlert()) { + Accessible* ancestor = aContainer; + do { + if (ancestor->IsAlert()) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor); + break; + } + } + while ((ancestor = ancestor->Parent())); + } +} + +void +DocAccessible::UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNode) +{ + // If child node is not accessible then look for its accessible children. + Accessible* child = GetAccessible(aChildNode); +#ifdef A11Y_LOG + logging::TreeInfo("process content removal", 0, + "container", aContainer, "child", aChildNode); +#endif + + TreeMutation mt(aContainer); + if (child) { + RefPtr<Accessible> kungFuDeathGripChild(child); + mt.BeforeRemoval(child); + if (child->IsDefunct()) { + return; // event coalescence may kill us + } + + MOZ_ASSERT(aContainer == child->Parent(), "Wrong parent"); + aContainer->RemoveChild(child); + UncacheChildrenInSubtree(child); + mt.Done(); + return; + } + + TreeWalker walker(aContainer, aChildNode, TreeWalker::eWalkCache); + while (Accessible* child = walker.Next()) { + RefPtr<Accessible> kungFuDeathGripChild(child); + mt.BeforeRemoval(child); + if (child->IsDefunct()) { + return; // event coalescence may kill us + } + + MOZ_ASSERT(aContainer == child->Parent(), "Wrong parent"); + aContainer->RemoveChild(child); + UncacheChildrenInSubtree(child); + } + mt.Done(); +} + +bool +DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) +{ + if (!aElement->HasID()) + return false; + + AttrRelProviderArray* list = + mDependentIDsHash.Get(nsDependentAtomString(aElement->GetID())); + if (list) { + for (uint32_t idx = 0; idx < list->Length(); idx++) { + if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) { + Accessible* owner = GetAccessible(list->ElementAt(idx)->mContent); + if (owner) { + mNotificationController->ScheduleRelocation(owner); + return true; + } + } + } + } + + return false; +} + +void +DocAccessible::ValidateARIAOwned() +{ + for (auto it = mARIAOwnsHash.Iter(); !it.Done(); it.Next()) { + Accessible* owner = it.Key(); + nsTArray<RefPtr<Accessible> >* children = it.UserData(); + + // Owner is about to die, put children back if applicable. + if (!mAccessibleCache.GetWeak(reinterpret_cast<void*>(owner)) || + !owner->IsInDocument()) { + PutChildrenBack(children, 0); + it.Remove(); + continue; + } + + for (uint32_t idx = 0; idx < children->Length(); idx++) { + Accessible* child = children->ElementAt(idx); + if (!child->IsInDocument()) { + children->RemoveElementAt(idx); + idx--; + continue; + } + + NS_ASSERTION(child->Parent(), "No parent for ARIA owned?"); + + // If DOM node doesn't have a frame anymore then shutdown its accessible. + if (child->Parent() && !child->GetFrame()) { + UpdateTreeOnRemoval(child->Parent(), child->GetContent()); + children->RemoveElementAt(idx); + idx--; + continue; + } + + NS_ASSERTION(child->Parent() == owner, + "Illigally stolen ARIA owned child!"); + } + + if (children->Length() == 0) { + it.Remove(); + } + } +} + +void +DocAccessible::DoARIAOwnsRelocation(Accessible* 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<Accessible> >* owned = mARIAOwnsHash.LookupOrAdd(aOwner); + IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns); + uint32_t idx = 0; + while (nsIContent* childEl = iter.NextElem()) { + Accessible* 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) { + 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 + + // Same child on same position, no change. + if (child->Parent() == aOwner && + child->IndexInParent() == static_cast<int32_t>(insertIdx)) { + MOZ_ASSERT(owned->ElementAt(idx) == child, "Not in sync!"); + idx++; + continue; + } + + MOZ_ASSERT(owned->SafeElementAt(idx) != child, "Already in place!"); + if (owned->IndexOf(child) < idx) { + continue; // ignore second entry of same ID + } + + // A new child is found, check for loops. + if (child->Parent() != aOwner) { + Accessible* parent = aOwner; + while (parent && parent != child && !parent->IsDoc()) { + parent = parent->Parent(); + } + // 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.LookupOrAdd(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<Accessible> >* aChildren, + uint32_t aStartIdx) +{ + MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index"); + + nsTArray<RefPtr<Accessible> > containers; + for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) { + Accessible* child = aChildren->ElementAt(idx); + if (!child->IsInDocument()) { + continue; + } + + // Remove the child from the owner + Accessible* owner = child->Parent(); + 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); + + int32_t idxInParent = -1; + Accessible* origContainer = GetContainerAccessible(child->GetContent()); + if (origContainer) { + TreeWalker walker(origContainer); + if (walker.Seek(child->GetContent())) { + Accessible* prevChild = walker.Prev(); + if (prevChild) { + idxInParent = prevChild->IndexInParent() + 1; + MOZ_ASSERT(origContainer == prevChild->Parent(), "Broken tree"); + origContainer = prevChild->Parent(); + } + else { + idxInParent = 0; + } + } + } + MoveChild(child, origContainer, idxInParent); + } + + aChildren->RemoveElementsAt(aStartIdx, aChildren->Length() - aStartIdx); +} + +bool +DocAccessible::MoveChild(Accessible* aChild, Accessible* aNewParent, + int32_t aIdxInParent) +{ + MOZ_ASSERT(aChild, "No child"); + MOZ_ASSERT(aChild->Parent(), "No parent"); + MOZ_ASSERT(aIdxInParent <= static_cast<int32_t>(aNewParent->ChildCount()), + "Wrong insertion point for a moving child"); + + Accessible* curParent = aChild->Parent(); + +#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<Accessible> >* children = mARIAOwnsHash.Get(curParent); + children->RemoveElement(aChild); + } + + NotificationController::MoveGuard mguard(mNotificationController); + + if (curParent == aNewParent) { + MOZ_ASSERT(aChild->IndexInParent() != aIdxInParent, "No move case"); + curParent->MoveChild(aIdxInParent, aChild); + +#ifdef A11Y_LOG + logging::TreeInfo("move child: parent tree after", + logging::eVerbose, curParent); +#endif + return true; + } + + if (!aNewParent->IsAcceptableChild(aChild->GetContent())) { + return false; + } + + TreeMutation rmut(curParent); + rmut.BeforeRemoval(aChild, TreeMutation::kNoShutdown); + curParent->RemoveChild(aChild); + rmut.Done(); + + // No insertion point for the child. + if (aIdxInParent == -1) { + return true; + } + + if (aIdxInParent > static_cast<int32_t>(aNewParent->ChildCount())) { + MOZ_ASSERT_UNREACHABLE("Wrong insertion point for a moving child"); + return true; + } + + TreeMutation imut(aNewParent); + aNewParent->InsertChildAt(aIdxInParent, 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(Accessible* aRoot, + Accessible** 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; + + Accessible* root = aRoot->IsHTMLCombobox() ? aRoot->FirstChild() : aRoot; + if (root->KidsFromDOM()) { + TreeMutation mt(root, TreeMutation::kNoEvents); + TreeWalker walker(root); + while (Accessible* child = walker.Next()) { + if (child->IsBoundToParent()) { + MoveChild(child, root, root->ChildCount()); + 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::DOCUMENT)) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot); + } +} + +void +DocAccessible::UncacheChildrenInSubtree(Accessible* aRoot) +{ + aRoot->mStateFlags |= eIsNotInDocument; + RemoveDependentIDsFor(aRoot); + + uint32_t count = aRoot->ContentChildCount(); + for (uint32_t idx = 0; idx < count; idx++) { + Accessible* child = aRoot->ContentChildAt(idx); + + // 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(Accessible* 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++) { + Accessible* 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(); + NS_ASSERTION(treeItem, "No document shell for document!"); + + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + treeItem->GetParent(getter_AddRefs(parentTreeItem)); + + // Not a root document. + if (parentTreeItem) { + // Return true if it's either: + // a) tab document; + nsCOMPtr<nsIDocShellTreeItem> rootTreeItem; + treeItem->GetRootTreeItem(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); +} diff --git a/accessible/generic/DocAccessible.h b/accessible/generic/DocAccessible.h new file mode 100644 index 0000000000..4bc6f03f08 --- /dev/null +++ b/accessible/generic/DocAccessible.h @@ -0,0 +1,718 @@ +/* -*- 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 "nsAutoPtr.h" +#include "nsClassHashtable.h" +#include "nsDataHashtable.h" +#include "nsIDocument.h" +#include "nsIDocumentObserver.h" +#include "nsIEditor.h" +#include "nsIObserver.h" +#include "nsIScrollPositionListener.h" +#include "nsITimer.h" +#include "nsIWeakReference.h" + +class nsAccessiblePivot; + +const uint32_t kDefaultCacheLength = 128; + +namespace mozilla { +namespace a11y { + +class DocManager; +class NotificationController; +class DocAccessibleChild; +class RelatedAccIterator; +template<class Class, class ... Args> +class TNotification; + +class DocAccessible : public HyperTextAccessibleWrap, + public nsIDocumentObserver, + public nsIObserver, + public nsIScrollPositionListener, + public nsSupportsWeakReference, + public nsIAccessiblePivotObserver +{ + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DocAccessible, Accessible) + + NS_DECL_NSIOBSERVER + NS_DECL_NSIACCESSIBLEPIVOTOBSERVER + +public: + + DocAccessible(nsIDocument* aDocument, nsIPresShell* aPresShell); + + // nsIScrollPositionListener + virtual void ScrollPositionWillChange(nscoord aX, nscoord aY) override {} + virtual void ScrollPositionDidChange(nscoord aX, nscoord aY) override; + + // nsIDocumentObserver + NS_DECL_NSIDOCUMENTOBSERVER + + // Accessible + virtual void Init(); + virtual void Shutdown() override; + virtual nsIFrame* GetFrame() const override; + virtual nsINode* GetNode() const override { return mDocumentNode; } + nsIDocument* DocumentNode() const { return mDocumentNode; } + + virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) override; + virtual void Description(nsString& aDescription) override; + virtual Accessible* FocusedChild() override; + virtual mozilla::a11y::role NativeRole() override; + virtual uint64_t NativeState() override; + virtual uint64_t NativeInteractiveState() const override; + virtual bool NativelyUnavailable() const override; + virtual void ApplyARIAState(uint64_t* aState) const override; + virtual already_AddRefed<nsIPersistentProperties> Attributes() override; + + virtual void TakeFocus() override; + +#ifdef A11Y_LOG + virtual nsresult HandleAccEvent(AccEvent* aEvent) override; +#endif + + virtual nsRect RelativeBounds(nsIFrame** aRelativeFrame) const override; + + // HyperTextAccessible + virtual already_AddRefed<nsIEditor> GetEditor() const override; + + // DocAccessible + + /** + * Return document URL. + */ + void URL(nsAString& aURL) const; + + /** + * Return DOM document title. + */ + void Title(nsString& aTitle) const { mDocumentNode->GetTitle(aTitle); } + + /** + * Return DOM document mime type. + */ + void MimeType(nsAString& aType) const { mDocumentNode->GetContentType(aType); } + + /** + * Return DOM document type. + */ + void DocType(nsAString& aType) const; + + /** + * Return virtual cursor associated with the document. + */ + nsIAccessiblePivot* VirtualCursor(); + + /** + * Return presentation shell for this document accessible. + */ + nsIPresShell* PresShell() const { return mPresShell; } + + /** + * Return the presentation shell's context. + */ + nsPresContext* PresContext() const { return mPresShell->GetPresContext(); } + + /** + * Return true if associated DOM document was loaded and isn't unloading. + */ + bool 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 nsIDocument::IsShowing() returns false. + return mDocumentNode && mDocumentNode->IsVisible() && + (mDocumentNode->IsShowing() || HasLoadState(eDOMLoaded)); + } + + /** + * 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, Accessible* aTarget); + void FireEventsOnInsertion(Accessible* aContainer); + + /** + * Fire value change event on the given accessible if applicable. + */ + void MaybeNotifyOfValueChange(Accessible* aAccessible); + + /** + * Get/set the anchor jump. + */ + Accessible* 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 Arg> + void HandleNotification(Class* aInstance, + typename TNotification<Class, Arg>::Callback aMethod, + Arg* aArg); + + /** + * 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 + */ + Accessible* GetAccessible(nsINode* aNode) const + { + return aNode == mDocumentNode ? + const_cast<DocAccessible*>(this) : mNodeToAccessibleMap.Get(aNode); + } + + /** + * 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(). + */ + Accessible* GetAccessibleEvenIfNotInMap(nsINode* aNode) const; + Accessible* 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. + */ + Accessible* 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. + */ + Accessible* GetAccessibleByUniqueIDInSubtree(void* aUniqueID); + + /** + * Return an accessible for the given DOM node or container accessible if + * the node is not accessible. + */ + Accessible* GetAccessibleOrContainer(nsINode* aNode) const; + + /** + * Return a container accessible for the given DOM node. + */ + Accessible* GetContainerAccessible(nsINode* aNode) const + { + return aNode ? GetAccessibleOrContainer(aNode->GetParentNode()) : nullptr; + } + + /** + * Return an accessible for the given node if any, or an immediate accessible + * container for it. + */ + Accessible* AccessibleOrTrueContainer(nsINode* aNode) const; + + /** + * Return an accessible for the given node or its first accessible descendant. + */ + Accessible* GetAccessibleOrDescendant(nsINode* aNode) const; + + /** + * Returns aria-owns seized child at the given index. + */ + Accessible* ARIAOwnedAt(Accessible* aParent, uint32_t aIndex) const + { + nsTArray<RefPtr<Accessible> >* children = mARIAOwnsHash.Get(aParent); + if (children) { + return children->SafeElementAt(aIndex); + } + return nullptr; + } + uint32_t ARIAOwnedCount(Accessible* aParent) const + { + nsTArray<RefPtr<Accessible> >* children = mARIAOwnsHash.Get(aParent); + return children ? children->Length() : 0; + } + + /** + * Return true if the given ID is referred by relation attribute. + * + * @note Different elements may share the same ID if they are hosted inside + * XBL bindings. Be careful the result of this method may be senseless + * while it's called for XUL elements (where XBL is used widely). + */ + bool IsDependentID(const nsAString& aID) const + { return mDependentIDsHash.Get(aID, nullptr); } + + /** + * 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(Accessible* aAccessible, + const nsRoleMapEntry* aRoleMapEntry); + + /** + * Remove from document and shutdown the given accessible. + */ + void UnbindFromDocument(Accessible* aAccessible); + + /** + * Notify the document accessible that content was inserted. + */ + void ContentInserted(nsIContent* aContainerNode, + nsIContent* aStartChildNode, + nsIContent* aEndChildNode); + + /** + * Notify the document accessible that content was removed. + */ + void ContentRemoved(Accessible* aContainer, nsIContent* aChildNode) + { + // Update the whole tree of this document accessible when the container is + // null (document element is removed). + UpdateTreeOnRemoval((aContainer ? aContainer : this), aChildNode); + } + void ContentRemoved(nsIContent* aContainerNode, nsIContent* aChildNode) + { + ContentRemoved(GetAccessibleOrContainer(aContainerNode), aChildNode); + } + + /** + * 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; } + +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(); + + /** + * Add/remove scroll listeners, @see nsIScrollPositionListener interface. + */ + void AddScrollListener(); + void RemoveScrollListener(); + + /** + * Append the given document accessible to this document's child document + * accessibles. + */ + bool AppendChildDocument(DocAccessible* aChildDocument) + { + return mChildDocuments.AppendElement(aChildDocument); + } + + /** + * 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(Accessible* aRelProvider, + nsIAtom* 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(Accessible* aRelProvider, + nsIAtom* 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, + nsIAtom* aAttribute); + + /** + * Fire accessible events when attribute is changed. + * + * @param aAccessible [in] accessible the DOM attribute is changed for + * @param aNameSpaceID [in] namespace of changed attribute + * @param aAttribute [in] changed attribute + */ + void AttributeChangedImpl(Accessible* aAccessible, + int32_t aNameSpaceID, nsIAtom* aAttribute); + + /** + * Fire accessible events when ARIA attribute is changed. + * + * @param aAccessible [in] accesislbe the DOM attribute is changed for + * @param aAttribute [in] changed attribute + */ + void ARIAAttributeChanged(Accessible* aAccessible, nsIAtom* aAttribute); + + /** + * Process ARIA active-descendant attribute change. + */ + void ARIAActiveDescendantChanged(Accessible* aAccessible); + + /** + * Update the accessible tree for inserted content. + */ + void ProcessContentInserted(Accessible* aContainer, + const nsTArray<nsCOMPtr<nsIContent> >* aInsertedContent); + void ProcessContentInserted(Accessible* 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(); + + /** + * Update the accessible tree for content removal. + */ + void UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNode); + + /** + * Validates all aria-owns connections and updates the tree accordingly. + */ + void ValidateARIAOwned(); + + /** + * Steals or puts back accessible subtrees. + */ + void DoARIAOwnsRelocation(Accessible* aOwner); + + /** + * Moves children back under their original parents. + */ + void PutChildrenBack(nsTArray<RefPtr<Accessible> >* aChildren, + uint32_t aStartIdx); + + bool MoveChild(Accessible* aChild, Accessible* 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(Accessible* aRoot, + Accessible** aFocusedAcc = nullptr); + void CreateSubtree(Accessible* aRoot); + + /** + * Remove accessibles in subtree from node to accessible map. + */ + void UncacheChildrenInSubtree(Accessible* 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(Accessible* 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) { mIPCDoc = 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); + +protected: + + /** + * State and property flags, kept by mDocFlags. + */ + enum { + // Whether scroll listeners were added. + eScrollInitialized = 1 << 0, + + // Whether the document is a tab document. + eTabDocument = 1 << 1 + }; + + /** + * Cache of accessibles within this document accessible. + */ + AccessibleHashtable mAccessibleCache; + nsDataHashtable<nsPtrHashKey<const nsINode>, Accessible*> + mNodeToAccessibleMap; + + nsIDocument* mDocumentNode; + nsCOMPtr<nsITimer> mScrollWatchTimer; + uint16_t mScrollPositionChangedTicks; // Used for tracking scroll events + + /** + * Bit mask of document load states (@see LoadState). + */ + uint32_t mLoadState : 3; + + /** + * Bit mask of other states and props. + */ + uint32_t mDocFlags : 28; + + /** + * 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. + */ + union { + // ARIA attribute value + nsIAtom* mARIAAttrOldValue; + + // True if the accessible state bit was on + bool mStateBitWasOn; + }; + + 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(nsIAtom* aRelAttr, nsIContent* aContent) : + mRelAttr(aRelAttr), mContent(aContent) { } + + nsIAtom* mRelAttr; + nsCOMPtr<nsIContent> mContent; + + private: + AttrRelProvider(); + AttrRelProvider(const AttrRelProvider&); + AttrRelProvider& operator =(const AttrRelProvider&); + }; + + /** + * The cache of IDs pointed by relation attributes. + */ + typedef nsTArray<nsAutoPtr<AttrRelProvider> > AttrRelProviderArray; + nsClassHashtable<nsStringHashKey, AttrRelProviderArray> + mDependentIDsHash; + + 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<Accessible>, nsTArray<RefPtr<Accessible> > > + mARIAOwnsHash; + + /** + * Used to process notification from core and accessible events. + */ + RefPtr<NotificationController> mNotificationController; + friend class EventTree; + friend class NotificationController; + +private: + + nsIPresShell* mPresShell; + + // Exclusively owned by IPDL so don't manually delete it! + DocAccessibleChild* mIPCDoc; +}; + +inline DocAccessible* +Accessible::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..0f27500702 --- /dev/null +++ b/accessible/generic/FormControlAccessible.cpp @@ -0,0 +1,193 @@ +/* -*- 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 "Role.h" + +#include "mozilla/FloatingPoint.h" +#include "nsIDOMHTMLFormElement.h" +#include "nsIDOMXULElement.h" +#include "nsIDOMXULControlElement.h" + +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// ProgressMeterAccessible +//////////////////////////////////////////////////////////////////////////////// + +template class mozilla::a11y::ProgressMeterAccessible<1>; +template class mozilla::a11y::ProgressMeterAccessible<100>; + +//////////////////////////////////////////////////////////////////////////////// +// Accessible + +template<int Max> +role +ProgressMeterAccessible<Max>::NativeRole() +{ + return roles::PROGRESSBAR; +} + +template<int Max> +uint64_t +ProgressMeterAccessible<Max>::NativeState() +{ + uint64_t state = LeafAccessible::NativeState(); + + // An undetermined progressbar (i.e. without a value) has a mixed state. + nsAutoString attrValue; + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::value, attrValue); + + if (attrValue.IsEmpty()) + state |= states::MIXED; + + return state; +} + +//////////////////////////////////////////////////////////////////////////////// +// ProgressMeterAccessible<Max>: Widgets + +template<int Max> +bool +ProgressMeterAccessible<Max>::IsWidget() const +{ + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// ProgressMeterAccessible<Max>: Value + +template<int Max> +void +ProgressMeterAccessible<Max>::Value(nsString& aValue) +{ + LeafAccessible::Value(aValue); + if (!aValue.IsEmpty()) + return; + + double maxValue = MaxValue(); + if (IsNaN(maxValue) || maxValue == 0) + return; + + double curValue = CurValue(); + if (IsNaN(curValue)) + return; + + // Treat the current value bigger than maximum as 100%. + double percentValue = (curValue < maxValue) ? + (curValue / maxValue) * 100 : 100; + + aValue.AppendFloat(percentValue); + aValue.Append('%'); +} + +template<int Max> +double +ProgressMeterAccessible<Max>::MaxValue() const +{ + double value = LeafAccessible::MaxValue(); + if (!IsNaN(value)) + return value; + + nsAutoString strValue; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::max, strValue)) { + nsresult result = NS_OK; + value = strValue.ToDouble(&result); + if (NS_SUCCEEDED(result)) + return value; + } + + return Max; +} + +template<int Max> +double +ProgressMeterAccessible<Max>::MinValue() const +{ + double value = LeafAccessible::MinValue(); + return IsNaN(value) ? 0 : value; +} + +template<int Max> +double +ProgressMeterAccessible<Max>::Step() const +{ + double value = LeafAccessible::Step(); + return IsNaN(value) ? 0 : value; +} + +template<int Max> +double +ProgressMeterAccessible<Max>::CurValue() const +{ + double value = LeafAccessible::CurValue(); + if (!IsNaN(value)) + return value; + + nsAutoString attrValue; + if (!mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::value, attrValue)) + return UnspecifiedNaN<double>(); + + nsresult error = NS_OK; + value = attrValue.ToDouble(&error); + return NS_FAILED(error) ? UnspecifiedNaN<double>() : value; +} + +template<int Max> +bool +ProgressMeterAccessible<Max>::SetCurValue(double aValue) +{ + return false; // progress meters are readonly. +} + +//////////////////////////////////////////////////////////////////////////////// +// RadioButtonAccessible +//////////////////////////////////////////////////////////////////////////////// + +RadioButtonAccessible:: + RadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc) : + LeafAccessible(aContent, aDoc) +{ +} + +uint8_t +RadioButtonAccessible::ActionCount() +{ + return 1; +} + +void +RadioButtonAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) +{ + if (aIndex == eAction_Click) + aName.AssignLiteral("select"); +} + +bool +RadioButtonAccessible::DoAction(uint8_t aIndex) +{ + if (aIndex != eAction_Click) + return false; + + DoCommand(); + return true; +} + +role +RadioButtonAccessible::NativeRole() +{ + 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..59844e5530 --- /dev/null +++ b/accessible/generic/FormControlAccessible.h @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_A11Y_FormControlAccessible_H_ +#define MOZILLA_A11Y_FormControlAccessible_H_ + +#include "BaseAccessibles.h" + +namespace mozilla { +namespace a11y { + +/** + * Generic class used for progress meters. + */ +template<int Max> +class ProgressMeterAccessible : public LeafAccessible +{ +public: + ProgressMeterAccessible(nsIContent* aContent, DocAccessible* aDoc) : + LeafAccessible(aContent, aDoc) + { + // Ignore 'ValueChange' DOM event in lieu of @value attribute change + // notifications. + mStateFlags |= eHasNumericValue | eIgnoreDOMUIEvent; + mType = eProgressType; + } + + // Accessible + virtual void Value(nsString& aValue) override; + virtual mozilla::a11y::role NativeRole() override; + virtual uint64_t NativeState() override; + + // Value + 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) override; + + // Widgets + virtual bool IsWidget() const override; + +protected: + virtual ~ProgressMeterAccessible() {} +}; + +/** + * Generic class used for radio buttons. + */ +class RadioButtonAccessible : public LeafAccessible +{ + +public: + RadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc); + + // Accessible + virtual mozilla::a11y::role NativeRole() override; + + // ActionAccessible + virtual uint8_t ActionCount() override; + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + virtual bool DoAction(uint8_t aIndex) 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..1e8deac5da --- /dev/null +++ b/accessible/generic/HyperTextAccessible-inl.h @@ -0,0 +1,180 @@ +/* -*- 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 "nsIEditor.h" +#include "nsIPersistentProperties2.h" +#include "nsIPlaintextEditor.h" +#include "nsFrameSelection.h" + +namespace mozilla { +namespace a11y { + +inline bool +HyperTextAccessible::IsValidOffset(int32_t aOffset) +{ + index_t offset = ConvertMagicOffset(aOffset); + return offset.IsValid() && offset <= CharacterCount(); +} + +inline bool +HyperTextAccessible::IsValidRange(int32_t aStartOffset, int32_t aEndOffset) +{ + index_t startOffset = ConvertMagicOffset(aStartOffset); + index_t endOffset = ConvertMagicOffset(aEndOffset); + return startOffset.IsValid() && endOffset.IsValid() && + startOffset <= endOffset && endOffset <= CharacterCount(); +} + +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) +{ + // We need to call DeleteText() even if there is no contents because we need + // to ensure to move focus to the editor via SetSelectionRange() called in + // DeleteText(). + DeleteText(0, CharacterCount()); + + nsCOMPtr<nsIEditor> editor = GetEditor(); + nsCOMPtr<nsIPlaintextEditor> plaintextEditor(do_QueryInterface(editor)); + if (!plaintextEditor) { + return; + } + + // DeleteText() may cause inserting <br> element in some cases. Let's + // select all again and replace whole contents. + editor->SelectAll(); + + plaintextEditor->InsertText(aText); +} + +inline void +HyperTextAccessible::InsertText(const nsAString& aText, int32_t aPosition) +{ + nsCOMPtr<nsIEditor> editor = GetEditor(); + nsCOMPtr<nsIPlaintextEditor> peditor(do_QueryInterface(editor)); + if (peditor) { + SetSelectionRange(aPosition, aPosition); + peditor->InsertText(aText); + } +} + +inline void +HyperTextAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) + { + nsCOMPtr<nsIEditor> editor = GetEditor(); + if (editor) { + SetSelectionRange(aStartPos, aEndPos); + editor->Copy(); + } + } + +inline void +HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) + { + nsCOMPtr<nsIEditor> editor = GetEditor(); + if (editor) { + SetSelectionRange(aStartPos, aEndPos); + editor->Cut(); + } + } + +inline void +HyperTextAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) +{ + nsCOMPtr<nsIEditor> editor = GetEditor(); + if (editor) { + SetSelectionRange(aStartPos, aEndPos); + editor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); + } +} + +inline void +HyperTextAccessible::PasteText(int32_t aPosition) +{ + nsCOMPtr<nsIEditor> editor = GetEditor(); + if (editor) { + SetSelectionRange(aPosition, aPosition); + editor->Paste(nsIClipboard::kGlobalClipboard); + } +} + +inline index_t +HyperTextAccessible::ConvertMagicOffset(int32_t aOffset) const +{ + if (aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) + return CharacterCount(); + + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) + return CaretOffset(); + + return aOffset; +} + +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..059c273726 --- /dev/null +++ b/accessible/generic/HyperTextAccessible.cpp @@ -0,0 +1,2230 @@ +/* -*- 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 "Accessible-inl.h" +#include "nsAccessibilityService.h" +#include "nsIAccessibleTypes.h" +#include "DocAccessible.h" +#include "HTMLListAccessible.h" +#include "Role.h" +#include "States.h" +#include "TextAttrs.h" +#include "TextRange.h" +#include "TreeWalker.h" + +#include "nsCaret.h" +#include "nsContentUtils.h" +#include "nsFocusManager.h" +#include "nsIDOMRange.h" +#include "nsIEditingSession.h" +#include "nsContainerFrame.h" +#include "nsFrameSelection.h" +#include "nsILineIterator.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPersistentProperties2.h" +#include "nsIScrollableFrame.h" +#include "nsIServiceManager.h" +#include "nsITextControlElement.h" +#include "nsIMathMLFrame.h" +#include "nsTextFragment.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/dom/Element.h" +#include "mozilla/EventStates.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/MathAlgorithms.h" +#include "gfxSkipChars.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// HyperTextAccessible +//////////////////////////////////////////////////////////////////////////////// + +HyperTextAccessible:: + HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc) : + AccessibleWrap(aNode, aDoc) +{ + mType = eHyperTextType; + mGenericTypes |= eHyperText; +} + +NS_IMPL_ISUPPORTS_INHERITED0(HyperTextAccessible, Accessible) + +role +HyperTextAccessible::NativeRole() +{ + a11y::role r = GetAccService()->MarkupRole(mContent); + if (r != roles::NOTHING) + return r; + + nsIFrame* frame = GetFrame(); + if (frame && frame->GetType() == nsGkAtoms::inlineFrame) + return roles::TEXT; + + return roles::TEXT_CONTAINER; +} + +uint64_t +HyperTextAccessible::NativeState() +{ + uint64_t states = AccessibleWrap::NativeState(); + + if (mContent->AsElement()->State().HasState(NS_EVENT_STATE_MOZ_READWRITE)) { + 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; + } + + if (HasChildren()) + states |= states::SELECTABLE_TEXT; + + return states; +} + +nsIntRect +HyperTextAccessible::GetBoundsInFrame(nsIFrame* aFrame, + uint32_t aStartRenderedOffset, + uint32_t aEndRenderedOffset) +{ + nsPresContext* presContext = mDoc->PresContext(); + if (aFrame->GetType() != nsGkAtoms::textFrame) { + return aFrame->GetScreenRectInAppUnits(). + ToNearestPixels(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, nsIntRect()); + rv = RenderedToContentOffset(aFrame, aEndRenderedOffset, &endContentOffset); + NS_ENSURE_SUCCESS(rv, nsIntRect()); + + 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, nsIntRect()); + + 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 + int32_t startFrameTextOffset, endFrameTextOffset; + frame->GetOffsets(startFrameTextOffset, endFrameTextOffset); + 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, nsIntRect()); + + // Use the point for the end offset to calculate the width + nsPoint frameTextEndPoint; + rv = frame->GetPointFromOffset(startContentOffset + frameSubStringLength, &frameTextEndPoint); + NS_ENSURE_SUCCESS(rv, nsIntRect()); + + frameScreenRect.x += std::min(frameTextStartPoint.x, frameTextEndPoint.x); + frameScreenRect.width = 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 screenRect.ToNearestPixels(presContext->AppUnitsPerDevPixel()); +} + +void +HyperTextAccessible::TextSubstring(int32_t aStartOffset, int32_t aEndOffset, + nsAString& aText) +{ + aText.Truncate(); + + 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; + } + + int32_t startChildIdx = GetChildIndexAtOffset(startOffset); + if (startChildIdx == -1) + return; + + int32_t endChildIdx = GetChildIndexAtOffset(endOffset); + if (endChildIdx == -1) + return; + + if (startChildIdx == endChildIdx) { + int32_t childOffset = GetChildOffset(startChildIdx); + if (childOffset == -1) + return; + + Accessible* child = GetChildAt(startChildIdx); + child->AppendTextTo(aText, startOffset - childOffset, + endOffset - startOffset); + return; + } + + int32_t startChildOffset = GetChildOffset(startChildIdx); + if (startChildOffset == -1) + return; + + Accessible* startChild = GetChildAt(startChildIdx); + startChild->AppendTextTo(aText, startOffset - startChildOffset); + + for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx; childIdx++) { + Accessible* child = GetChildAt(childIdx); + child->AppendTextTo(aText); + } + + int32_t endChildOffset = GetChildOffset(endChildIdx); + if (endChildOffset == -1) + return; + + Accessible* endChild = GetChildAt(endChildIdx); + endChild->AppendTextTo(aText, 0, endOffset - endChildOffset); +} + +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->IsNodeOfType(nsINode::eTEXT)) { + // 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(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) + Accessible* descendant = nullptr; + if (findNode) { + nsCOMPtr<nsIContent> findContent(do_QueryInterface(findNode)); + if (findContent && findContent->IsHTMLElement() && + findContent->NodeInfo()->Equals(nsGkAtoms::br) && + findContent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::mozeditorbogusnode, + nsGkAtoms::_true, + eIgnoreCase)) { + // This <br> is the hacky "bogus node" used when there is no text in a control + return 0; + } + + descendant = mDoc->GetAccessible(findNode); + if (!descendant && findNode->IsContent()) { + Accessible* 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(Accessible* aDescendant, + uint32_t aOffset, bool aIsEndOffset) const +{ + // From the descendant, go up and get the immediate child of this hypertext. + uint32_t offset = aOffset; + Accessible* descendant = aDescendant; + while (descendant) { + Accessible* parent = descendant->Parent(); + 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) + 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(); +} + +/** + * GetElementAsContentOf() returns a content representing an element which is + * or includes aNode. + * + * XXX This method is enough to retrieve ::before or ::after pseudo element. + * So, if you want to use this for other purpose, you might need to check + * ancestors too. + */ +static nsIContent* GetElementAsContentOf(nsINode* aNode) +{ + if (aNode->IsElement()) { + return aNode->AsContent(); + } + nsIContent* parent = aNode->GetParent(); + return parent && parent->IsElement() ? parent : nullptr; +} + +bool +HyperTextAccessible::OffsetsToDOMRange(int32_t aStartOffset, int32_t aEndOffset, + nsRange* aRange) +{ + DOMPoint startPoint = OffsetToDOMPoint(aStartOffset); + if (!startPoint.node) + return false; + + // HyperTextAccessible manages pseudo elements generated by ::before or + // ::after. However, contents of them are not in the DOM tree normally. + // Therefore, they are not selectable and editable. So, when this creates + // a DOM range, it should not start from nor end in any pseudo contents. + + nsIContent* container = GetElementAsContentOf(startPoint.node); + DOMPoint startPointForDOMRange = + ClosestNotGeneratedDOMPoint(startPoint, container); + aRange->SetStart(startPointForDOMRange.node, startPointForDOMRange.idx); + + // If the caller wants collapsed range, let's collapse the range to its start. + if (aStartOffset == aEndOffset) { + aRange->Collapse(true); + return true; + } + + DOMPoint endPoint = OffsetToDOMPoint(aEndOffset); + if (!endPoint.node) + return false; + + if (startPoint.node != endPoint.node) { + container = GetElementAsContentOf(endPoint.node); + } + + DOMPoint endPointForDOMRange = + ClosestNotGeneratedDOMPoint(endPoint, container); + aRange->SetEnd(endPointForDOMRange.node, endPointForDOMRange.idx); + return true; +} + +DOMPoint +HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) +{ + // 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) { + nsCOMPtr<nsIEditor> editor = GetEditor(); + if (editor) { + bool isEmpty = false; + editor->GetDocumentIsEmpty(&isEmpty); + if (isEmpty) { + nsCOMPtr<nsIDOMElement> editorRootElm; + editor->GetRootElement(getter_AddRefs(editorRootElm)); + + nsCOMPtr<nsINode> editorRoot(do_QueryInterface(editorRootElm)); + return DOMPoint(editorRoot, 0); + } + } + } + + int32_t childIdx = GetChildIndexAtOffset(aOffset); + if (childIdx == -1) + return DOMPoint(); + + Accessible* child = GetChildAt(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->IndexOf(node) + innerOffset) : + DOMPoint(); +} + +DOMPoint +HyperTextAccessible::ClosestNotGeneratedDOMPoint(const DOMPoint& aDOMPoint, + nsIContent* aElementContent) +{ + MOZ_ASSERT(aDOMPoint.node, "The node must not be null"); + + // ::before pseudo element + if (aElementContent && + aElementContent->IsGeneratedContentContainerForBefore()) { + MOZ_ASSERT(aElementContent->GetParent(), + "::before must have parent element"); + // The first child of its parent (i.e., immediately after the ::before) is + // good point for a DOM range. + return DOMPoint(aElementContent->GetParent(), 0); + } + + // ::after pseudo element + if (aElementContent && + aElementContent->IsGeneratedContentContainerForAfter()) { + MOZ_ASSERT(aElementContent->GetParent(), + "::after must have parent element"); + // The end of its parent (i.e., immediately before the ::after) is good + // point for a DOM range. + return DOMPoint(aElementContent->GetParent(), + aElementContent->GetParent()->GetChildCount()); + } + + return aDOMPoint; +} + +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; + Accessible* 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->GetChildAt(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->GetType() == nsGkAtoms::textFrame, "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, aWordMovementType); + nsresult rv = frameAtOffset->PeekOffset(&pos); + + // PeekOffset fails on last/first lines of the text in certain cases. + if (NS_FAILED(rv) && aAmount == eSelectLine) { + pos.mAmount = (aDirection == eDirNext) ? 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 (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) { + Accessible* prevOffsetChild = GetChildAtOffset(hyperTextOffset - 1); + if (prevOffsetChild == AsHTMLListItem()->Bullet()) + return 0; + } + } + + return hyperTextOffset; +} + +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). + return FindOffset(aOffset, eDirPrevious, eSelectBeginLine); + + 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); + if (tmpOffset == CharacterCount()) + return tmpOffset; + + return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine); + } + + 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; +} + +void +HyperTextAccessible::TextBeforeOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText) +{ + *aStartOffset = *aEndOffset = 0; + aText.Truncate(); + + 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) +{ + *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; + } +} + +void +HyperTextAccessible::TextAfterOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText) +{ + *aStartOffset = *aEndOffset = 0; + aText.Truncate(); + + 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<nsIPersistentProperties> +HyperTextAccessible::TextAttributes(bool aIncludeDefAttrs, int32_t aOffset, + int32_t* aStartOffset, + int32_t* 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. + + *aStartOffset = *aEndOffset = 0; + index_t offset = ConvertMagicOffset(aOffset); + if (!offset.IsValid() || offset > CharacterCount()) { + NS_ERROR("Wrong in offset!"); + return nullptr; + } + + nsCOMPtr<nsIPersistentProperties> attributes = + do_CreateInstance(NS_PERSISTENTPROPERTIES_CONTRACTID); + + Accessible* 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(); + } + return nullptr; + } + + 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->GetType() == nsGkAtoms::textFrame) { + int32_t nodeOffset = 0; + RenderedToContentOffset(offsetFrame, offsetInAcc, &nodeOffset); + + // Set 'misspelled' text attribute. + GetSpellTextAttr(accAtOffset->GetNode(), nodeOffset, + &startOffset, &endOffset, attributes); + } + + *aStartOffset = startOffset; + *aEndOffset = endOffset; + return attributes.forget(); +} + +already_AddRefed<nsIPersistentProperties> +HyperTextAccessible::DefaultTextAttributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = + do_CreateInstance(NS_PERSISTENTPROPERTIES_CONTRACTID); + + TextAttrsMgr textAttrsMgr(this); + textAttrsMgr.GetAttributes(attributes); + return attributes.forget(); +} + +int32_t +HyperTextAccessible::GetLevelInternal() +{ + if (mContent->IsHTMLElement(nsGkAtoms::h1)) + return 1; + if (mContent->IsHTMLElement(nsGkAtoms::h2)) + return 2; + if (mContent->IsHTMLElement(nsGkAtoms::h3)) + return 3; + if (mContent->IsHTMLElement(nsGkAtoms::h4)) + return 4; + if (mContent->IsHTMLElement(nsGkAtoms::h5)) + return 5; + if (mContent->IsHTMLElement(nsGkAtoms::h6)) + return 6; + + return AccessibleWrap::GetLevelInternal(); +} + +void +HyperTextAccessible::SetMathMLXMLRoles(nsIPersistentProperties* aAttributes) +{ + // Add MathML xmlroles based on the position inside the parent. + Accessible* parent = Parent(); + 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 (!PrevSibling()) { + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + nsGkAtoms::open_fence); + } else if (!NextSibling()) { + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + nsGkAtoms::close_fence); + } + } + if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) { + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + nsGkAtoms::separator_); + } + } + } + break; + case roles::MATHML_FRACTION: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + IndexInParent() == 0 ? + nsGkAtoms::numerator : + nsGkAtoms::denominator); + break; + case roles::MATHML_ROOT: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : + nsGkAtoms::root_index); + break; + case roles::MATHML_SUB: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : + nsGkAtoms::subscript); + break; + case roles::MATHML_SUP: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : + nsGkAtoms::superscript); + break; + case roles::MATHML_SUB_SUP: { + int32_t index = IndexInParent(); + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + index == 0 ? nsGkAtoms::base : + (index == 1 ? nsGkAtoms::subscript : + nsGkAtoms::superscript)); + } break; + case roles::MATHML_UNDER: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : + nsGkAtoms::underscript); + break; + case roles::MATHML_OVER: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + IndexInParent() == 0 ? nsGkAtoms::base : + nsGkAtoms::overscript); + break; + case roles::MATHML_UNDER_OVER: { + int32_t index = IndexInParent(); + nsAccUtils::SetAccAttr(aAttributes, 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. + nsAccUtils::SetAccAttr(aAttributes, 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) { + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + subscript ? + nsGkAtoms::subscript : + nsGkAtoms::superscript); + } else { + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles, + subscript ? + nsGkAtoms::presubscript : + nsGkAtoms::presuperscript); + } + break; + } + subscript = !subscript; + } + } + } + } break; + default: + break; + } + } +} + +already_AddRefed<nsIPersistentProperties> +HyperTextAccessible::NativeAttributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = + AccessibleWrap::NativeAttributes(); + + // 'formatting' attribute is deprecated, 'display' attribute should be + // instead. + nsIFrame *frame = GetFrame(); + if (frame && frame->GetType() == nsGkAtoms::blockFrame) { + nsAutoString unused; + attributes->SetStringProperty(NS_LITERAL_CSTRING("formatting"), + NS_LITERAL_STRING("block"), unused); + } + + if (FocusMgr()->IsFocused(this)) { + int32_t lineNumber = CaretLineNumber(); + if (lineNumber >= 1) { + nsAutoString strLineNumber; + strLineNumber.AppendInt(lineNumber); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::lineNumber, strLineNumber); + } + } + + if (HasOwnContent()) { + GetAccService()->MarkupAttributes(mContent, attributes); + if (mContent->IsMathMLElement()) + SetMathMLXMLRoles(attributes); + } + + return attributes.forget(); +} + +nsIAtom* +HyperTextAccessible::LandmarkRole() const +{ + if (!HasOwnContent()) + return nullptr; + + // For the html landmark elements we expose them like we do ARIA landmarks to + // make AT navigation schemes "just work". + if (mContent->IsHTMLElement(nsGkAtoms::nav)) { + return nsGkAtoms::navigation; + } + + if (mContent->IsAnyOfHTMLElements(nsGkAtoms::header, + nsGkAtoms::footer)) { + // Only map header and footer if they are not descendants of an article + // or section tag. + nsIContent* parent = mContent->GetParent(); + while (parent) { + if (parent->IsAnyOfHTMLElements(nsGkAtoms::article, nsGkAtoms::section)) { + break; + } + parent = parent->GetParent(); + } + + // No article or section elements found. + if (!parent) { + if (mContent->IsHTMLElement(nsGkAtoms::header)) { + return nsGkAtoms::banner; + } + + if (mContent->IsHTMLElement(nsGkAtoms::footer)) { + return nsGkAtoms::contentinfo; + } + } + return nullptr; + } + + if (mContent->IsHTMLElement(nsGkAtoms::aside)) { + return nsGkAtoms::complementary; + } + + if (mContent->IsHTMLElement(nsGkAtoms::main)) { + return nsGkAtoms::main; + } + + return nullptr; +} + +int32_t +HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType) +{ + nsIFrame* hyperFrame = GetFrame(); + if (!hyperFrame) + return -1; + + nsIntPoint coords = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, + this); + + nsPresContext* presContext = mDoc->PresContext(); + nsPoint coordsInAppUnits = + 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++) { + Accessible* 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->GetType() == nsGkAtoms::textFrame) { + 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 +} + +nsIntRect +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 nsIntRect(); + } + + + int32_t childIdx = GetChildIndexAtOffset(startOffset); + if (childIdx == -1) + return nsIntRect(); + + nsIntRect bounds; + int32_t prevOffset = GetChildOffset(childIdx); + int32_t offset1 = startOffset - prevOffset; + + while (childIdx < static_cast<int32_t>(ChildCount())) { + nsIFrame* frame = GetChildAt(childIdx++)->GetFrame(); + if (!frame) { + NS_NOTREACHED("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; + } + + nsAccUtils::ConvertScreenCoordsTo(&bounds.x, &bounds.y, aCoordType, this); + return bounds; +} + +already_AddRefed<nsIEditor> +HyperTextAccessible::GetEditor() const +{ + if (!mContent->HasFlag(NODE_IS_EDITABLE)) { + // If we're inside an editable container, then return that container's editor + Accessible* ancestor = Parent(); + 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->Parent(); + } + + return nullptr; + } + + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent); + nsCOMPtr<nsIEditingSession> editingSession; + docShell->GetEditingSession(getter_AddRefs(editingSession)); + if (!editingSession) + return nullptr; // No editing session interface + + nsCOMPtr<nsIEditor> editor; + nsIDocument* docNode = mDoc->DocumentNode(); + editingSession->GetEditorForWindow(docNode->GetWindow(), + getter_AddRefs(editor)); + return editor.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. + nsCOMPtr<nsIEditor> editor = 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(); + + dom::Selection* domSel = DOMSelection(); + NS_ENSURE_STATE(domSel); + + // Set up the selection. + for (int32_t idx = domSel->RangeCount() - 1; idx > 0; idx--) + domSel->RemoveRange(domSel->GetRangeAt(idx)); + SetSelectionBoundsAt(0, aStartPos, aEndPos); + + // 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); + nsIDocument* docNode = mDoc->DocumentNode(); + NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE); + nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow(); + nsCOMPtr<nsIDOMElement> 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->IsNodeOfType(nsINode::eTEXT) ? 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); + + int32_t lineNumber = 1; + nsAutoLineIterator lineIterForCaret; + 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) { + nsAutoLineIterator 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; + } + + NS_NOTREACHED("DOM ancestry had this hypertext but frame ancestry didn't"); + return lineNumber; +} + +LayoutDeviceIntRect +HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) +{ + *aWidget = nullptr; + + RefPtr<nsCaret> caret = mDoc->PresShell()->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())); + // ((content screen origin) - (content offset in the widget)) = widget origin on the screen + 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. + nsIntRect charRect = CharBounds(CaretOffset(), + nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE); + if (!charRect.IsEmpty()) { + caretRect.height -= charRect.y - caretRect.y; + caretRect.y = 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; + + nsCOMPtr<nsINode> startNode = GetNode(); + + nsCOMPtr<nsIEditor> editor = GetEditor(); + if (editor) { + nsCOMPtr<nsIDOMElement> editorRoot; + editor->GetRootElement(getter_AddRefs(editorRoot)); + startNode = do_QueryInterface(editorRoot); + } + + 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 + uint32_t numRanges = aRanges->Length(); + for (uint32_t idx = 0; idx < numRanges; idx ++) { + if ((*aRanges)[idx]->Collapsed()) { + aRanges->RemoveElementAt(idx); + --numRanges; + --idx; + } + } +} + +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->GetStartParent(); + nsINode* endNode = range->GetEndParent(); + int32_t startOffset = range->StartOffset(), endOffset = range->EndOffset(); + + // Make sure start is before end, by swapping DOM points. This occurs when + // the user selects backwards in the text. + int32_t rangeCompare = nsContentUtils::ComparePoints(endNode, endOffset, + startNode, startOffset); + if (rangeCompare < 0) { + nsINode* tempNode = startNode; + startNode = endNode; + endNode = tempNode; + int32_t tempOffset = startOffset; + startOffset = endOffset; + endOffset = tempOffset; + } + + if (!nsContentUtils::ContentIsDescendantOf(startNode, mContent)) + *aStartOffset = 0; + else + *aStartOffset = DOMPointToOffset(startNode, startOffset); + + if (!nsContentUtils::ContentIsDescendantOf(endNode, mContent)) + *aEndOffset = CharacterCount(); + else + *aEndOffset = DOMPointToOffset(endNode, 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() || + startOffset > endOffset || endOffset > CharacterCount()) { + NS_ERROR("Wrong in offset"); + return false; + } + + dom::Selection* domSel = DOMSelection(); + if (!domSel) + return false; + + RefPtr<nsRange> range; + uint32_t rangeCount = domSel->RangeCount(); + if (aSelectionNum == static_cast<int32_t>(rangeCount)) + range = new nsRange(mContent); + else + range = domSel->GetRangeAt(aSelectionNum); + + if (!range) + return false; + + if (!OffsetsToDOMRange(startOffset, endOffset, range)) + return false; + + // If new range was created then add it, otherwise notify selection listeners + // that existing selection range was changed. + if (aSelectionNum == static_cast<int32_t>(rangeCount)) + return NS_SUCCEEDED(domSel->AddRange(range)); + + domSel->RemoveRange(range); + return NS_SUCCEEDED(domSel->AddRange(range)); +} + +bool +HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) +{ + dom::Selection* domSel = DOMSelection(); + if (!domSel) + return false; + + if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) + return false; + + domSel->RemoveRange(domSel->GetRangeAt(aSelectionNum)); + return true; +} + +void +HyperTextAccessible::ScrollSubstringTo(int32_t aStartOffset, int32_t aEndOffset, + uint32_t aScrollType) +{ + RefPtr<nsRange> range = new nsRange(mContent); + if (OffsetsToDOMRange(aStartOffset, aEndOffset, range)) + nsCoreUtils::ScrollSubstringTo(GetFrame(), range, 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; + + nsIntPoint coords = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, + this); + + RefPtr<nsRange> range = new nsRange(mContent); + if (!OffsetsToDOMRange(aStartOffset, aEndOffset, range)) + return; + + nsPresContext* presContext = frame->PresContext(); + nsPoint coordsInAppUnits = + 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, range, + nsIPresShell::ScrollAxis(vPercent), + nsIPresShell::ScrollAxis(hPercent)); + 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 +{ + MOZ_ASSERT(aRanges->Length() == 0, "TextRange array supposed to be empty"); + + dom::Selection* sel = DOMSelection(); + if (!sel) + return; + + aRanges->SetCapacity(sel->RangeCount()); + + for (uint32_t idx = 0; idx < sel->RangeCount(); idx++) { + nsRange* DOMRange = sel->GetRangeAt(idx); + HyperTextAccessible* startParent = + nsAccUtils::GetTextContainer(DOMRange->GetStartParent()); + HyperTextAccessible* endParent = + nsAccUtils::GetTextContainer(DOMRange->GetEndParent()); + if (!startParent || !endParent) + continue; + + int32_t startOffset = + startParent->DOMPointToOffset(DOMRange->GetStartParent(), + DOMRange->StartOffset(), false); + int32_t endOffset = + endParent->DOMPointToOffset(DOMRange->GetEndParent(), + DOMRange->EndOffset(), true); + + TextRange tr(IsTextField() ? const_cast<HyperTextAccessible*>(this) : mDoc, + startParent, startOffset, endParent, endOffset); + *(aRanges->AppendElement()) = Move(tr); + } +} + +void +HyperTextAccessible::VisibleRanges(nsTArray<a11y::TextRange>* aRanges) const +{ +} + +void +HyperTextAccessible::RangeByChild(Accessible* aChild, + a11y::TextRange& aRange) const +{ + HyperTextAccessible* ht = aChild->AsHyperText(); + if (ht) { + aRange.Set(mDoc, ht, 0, ht, ht->CharacterCount()); + return; + } + + Accessible* child = aChild; + Accessible* parent = nullptr; + while ((parent = child->Parent()) && !(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 +{ + Accessible* child = mDoc->ChildAtPoint(aX, aY, eDeepestChild); + if (!child) + return; + + Accessible* parent = nullptr; + while ((parent = child->Parent()) && !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); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible public + +// Accessible protected +ENameValueFlag +HyperTextAccessible::NativeName(nsString& aName) +{ + // Check @alt attribute for invalid img elements. + bool hasImgAlt = false; + if (mContent->IsHTMLElement(nsGkAtoms::img)) { + hasImgAlt = mContent->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->GetAttr(kNameSpaceID_None, nsGkAtoms::title, aName)) + aName.CompressWhitespace(); + + return hasImgAlt ? eNoNameOnPurpose : eNameOK; +} + +void +HyperTextAccessible::Shutdown() +{ + mOffsets.Clear(); + AccessibleWrap::Shutdown(); +} + +bool +HyperTextAccessible::RemoveChild(Accessible* aAccessible) +{ + int32_t childIndex = aAccessible->IndexInParent(); + int32_t count = mOffsets.Length() - childIndex; + if (count > 0) + mOffsets.RemoveElementsAt(childIndex, count); + + return AccessibleWrap::RemoveChild(aAccessible); +} + +bool +HyperTextAccessible::InsertChildAt(uint32_t aIndex, Accessible* aChild) +{ + int32_t count = mOffsets.Length() - aIndex; + if (count > 0 ) { + mOffsets.RemoveElementsAt(aIndex, count); + } + return AccessibleWrap::InsertChildAt(aIndex, aChild); +} + +Relation +HyperTextAccessible::RelationByType(RelationType aType) +{ + Relation rel = Accessible::RelationByType(aType); + + switch (aType) { + case RelationType::NODE_CHILD_OF: + if (HasOwnContent() && mContent->IsMathMLElement()) { + Accessible* parent = Parent(); + 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_)) { + Accessible* base = GetChildAt(0); + Accessible* index = GetChildAt(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->GetType() == nsGkAtoms::textFrame, + "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::OFFSETS_IN_CONTENT_TEXT, + nsIFrame::TrailingWhitespace::DONT_TRIM_TRAILING_WHITESPACE); + *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->GetType() == nsGkAtoms::textFrame, + "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::OFFSETS_IN_RENDERED_TEXT, + nsIFrame::TrailingWhitespace::DONT_TRIM_TRAILING_WHITESPACE); + *aContentOffset = text.mOffsetWithinNodeText; + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// HyperTextAccessible public + +int32_t +HyperTextAccessible::GetChildOffset(uint32_t aChildIndex, + bool aInvalidateAfter) const +{ + if (aChildIndex == 0) { + if (aInvalidateAfter) + mOffsets.Clear(); + + return aChildIndex; + } + + int32_t count = mOffsets.Length() - aChildIndex; + if (count > 0) { + if (aInvalidateAfter) + mOffsets.RemoveElementsAt(aChildIndex, count); + + return mOffsets[aChildIndex - 1]; + } + + uint32_t lastOffset = mOffsets.IsEmpty() ? + 0 : mOffsets[mOffsets.Length() - 1]; + + while (mOffsets.Length() < aChildIndex) { + Accessible* child = mChildren[mOffsets.Length()]; + lastOffset += nsAccUtils::TextLength(child); + mOffsets.AppendElement(lastOffset); + } + + return mOffsets[aChildIndex - 1]; +} + +int32_t +HyperTextAccessible::GetChildIndexAtOffset(uint32_t aOffset) const +{ + uint32_t lastOffset = 0; + const uint32_t offsetCount = mOffsets.Length(); + + if (offsetCount > 0) { + lastOffset = mOffsets[offsetCount - 1]; + if (aOffset < lastOffset) { + size_t index; + if (BinarySearch(mOffsets, 0, offsetCount, aOffset, &index)) { + return (index < (offsetCount - 1)) ? index + 1 : index; + } + + return (index == offsetCount) ? -1 : index; + } + } + + uint32_t childCount = ChildCount(); + while (mOffsets.Length() < childCount) { + Accessible* child = GetChildAt(mOffsets.Length()); + lastOffset += nsAccUtils::TextLength(child); + mOffsets.AppendElement(lastOffset); + if (aOffset < lastOffset) + return mOffsets.Length() - 1; + } + + if (aOffset == lastOffset) + return mOffsets.Length() - 1; + + return -1; +} + +//////////////////////////////////////////////////////////////////////////////// +// HyperTextAccessible protected + +nsresult +HyperTextAccessible::GetDOMPointByFrameOffset(nsIFrame* aFrame, int32_t aOffset, + Accessible* 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->IndexOf(content) + 1; + aPoint->node = parent; + + } else if (aFrame->GetType() == nsGkAtoms::textFrame) { + 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->IndexOf(content); + aPoint->node = parent; + } + + return NS_OK; +} + +// HyperTextAccessible +void +HyperTextAccessible::GetSpellTextAttr(nsINode* aNode, + int32_t aNodeOffset, + uint32_t* aStartOffset, + uint32_t* aEndOffset, + nsIPersistentProperties* aAttributes) +{ + RefPtr<nsFrameSelection> fs = FrameSelection(); + if (!fs) + return; + + dom::Selection* domSel = fs->GetSelection(SelectionType::eSpellCheck); + if (!domSel) + return; + + int32_t rangeCount = domSel->RangeCount(); + if (rangeCount <= 0) + return; + + uint32_t startOffset = 0, endOffset = 0; + for (int32_t idx = 0; idx < rangeCount; idx++) { + nsRange* range = domSel->GetRangeAt(idx); + 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->GetEndParent(); + int32_t endNodeOffset = range->EndOffset(); + if (nsContentUtils::ComparePoints(aNode, aNodeOffset, + endNode, endNodeOffset) >= 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->GetStartParent(); + int32_t startNodeOffset = range->StartOffset(); + if (nsContentUtils::ComparePoints(startNode, startNodeOffset, aNode, + aNodeOffset) <= 0) { + startOffset = DOMPointToOffset(startNode, startNodeOffset); + + endOffset = DOMPointToOffset(endNode, endNodeOffset); + + if (startOffset > *aStartOffset) + *aStartOffset = startOffset; + + if (endOffset < *aEndOffset) + *aEndOffset = endOffset; + + if (aAttributes) { + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, + NS_LITERAL_STRING("spelling")); + } + + return; + } + + // This range came after the point. + endOffset = DOMPointToOffset(startNode, startNodeOffset); + + if (idx > 0) { + nsRange* prevRange = domSel->GetRangeAt(idx - 1); + startOffset = DOMPointToOffset(prevRange->GetEndParent(), + prevRange->EndOffset()); + } + + if (startOffset > *aStartOffset) + *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. + nsRange* prevRange = domSel->GetRangeAt(rangeCount - 1); + startOffset = DOMPointToOffset(prevRange->GetEndParent(), + prevRange->EndOffset()); + + if (startOffset > *aStartOffset) + *aStartOffset = startOffset; +} + +bool +HyperTextAccessible::IsTextRole() +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && + (roleMapEntry->role == roles::GRAPHIC || + roleMapEntry->role == roles::IMAGE_MAP || + roleMapEntry->role == roles::SLIDER || + roleMapEntry->role == roles::PROGRESSBAR || + roleMapEntry->role == roles::SEPARATOR)) + return false; + + return true; +} diff --git a/accessible/generic/HyperTextAccessible.h b/accessible/generic/HyperTextAccessible.h new file mode 100644 index 0000000000..78e73f042a --- /dev/null +++ b/accessible/generic/HyperTextAccessible.h @@ -0,0 +1,592 @@ +/* -*- 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 "nsIAccessibleText.h" +#include "nsIAccessibleTypes.h" +#include "nsDirection.h" +#include "WordMovementType.h" +#include "nsIFrame.h" + +#include "nsISelectionController.h" + +class nsFrameSelection; +class nsRange; +class nsIWidget; + +namespace mozilla { + +namespace dom { +class Selection; +} + +namespace a11y { + +class TextRange; + +struct DOMPoint { + DOMPoint() : node(nullptr), idx(0) { } + DOMPoint(nsINode* aNode, int32_t aIdx) : node(aNode), idx(aIdx) { } + + nsINode* node; + int32_t idx; +}; + +// This character marks where in the text returned via Text interface, +// that embedded object characters exist +const char16_t kEmbeddedObjectChar = 0xfffc; +const char16_t kImaginaryEmbeddedObjectChar = ' '; +const char16_t kForcedNewLineChar = '\n'; + +/** + * Special Accessible that knows how contain both text and embedded objects + */ +class HyperTextAccessible : public AccessibleWrap +{ +public: + HyperTextAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_DECL_ISUPPORTS_INHERITED + + // Accessible + virtual nsIAtom* LandmarkRole() const override; + virtual int32_t GetLevelInternal() override; + virtual already_AddRefed<nsIPersistentProperties> NativeAttributes() override; + virtual mozilla::a11y::role NativeRole() override; + virtual uint64_t NativeState() override; + + virtual void Shutdown() override; + virtual bool RemoveChild(Accessible* aAccessible) override; + virtual bool InsertChildAt(uint32_t aIndex, Accessible* aChild) override; + virtual Relation RelationByType(RelationType aType) override; + + // 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 count within this hypertext accessible. + */ + uint32_t LinkCount() + { return EmbeddedChildCount(); } + + /** + * Return link accessible at the given index. + */ + Accessible* LinkAt(uint32_t aIndex) + { + return GetEmbeddedChildAt(aIndex); + } + + /** + * Return index for the given link accessible. + */ + int32_t LinkIndexOf(Accessible* aLink) + { + return GetIndexOfEmbeddedChild(aLink); + } + + /** + * Return link accessible at the given text offset. + */ + int32_t LinkIndexAtOffset(uint32_t aOffset) + { + Accessible* child = GetChildAtOffset(aOffset); + return child ? LinkIndexOf(child) : -1; + } + + ////////////////////////////////////////////////////////////////////////////// + // 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 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(Accessible* aDescendant, uint32_t aOffset, + bool aIsEndOffset) const; + + /** + * Convert start and end hypertext offsets into DOM range. Note that if + * aStartOffset and/or aEndOffset is in generated content such as ::before or + * ::after, the result range excludes the generated content. See also + * ClosestNotGeneratedDOMPoint() for more information. + * + * @param aStartOffset [in] the given start hypertext offset + * @param aEndOffset [in] the given end hypertext offset + * @param aRange [in, out] the range whose bounds to set + * @return true if conversion was successful + */ + bool OffsetsToDOMRange(int32_t aStartOffset, int32_t aEndOffset, + nsRange* aRange); + + /** + * 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); + + /** + * Return true if the used ARIA role (if any) allows the hypertext accessible + * to expose text interfaces. + */ + bool IsTextRole(); + + ////////////////////////////////////////////////////////////////////////////// + // TextAccessible + + /** + * Return character count within the hypertext accessible. + */ + uint32_t CharacterCount() const + { return GetChildOffset(ChildCount()); } + + /** + * Get a character at the given offset (don't support magic offsets). + */ + bool CharAt(int32_t aOffset, nsAString& aChar, + int32_t* aStartOffset = nullptr, int32_t* aEndOffset = nullptr) + { + NS_ASSERTION(!aStartOffset == !aEndOffset, + "Offsets should be both defined or both undefined!"); + + int32_t childIdx = GetChildIndexAtOffset(aOffset); + if (childIdx == -1) + return false; + + Accessible* child = GetChildAt(childIdx); + child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1); + + if (aStartOffset && aEndOffset) { + *aStartOffset = aOffset; + *aEndOffset = aOffset + aChar.Length(); + } + return true; + } + + 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'); } + + /** + * Return text between given offsets. + */ + void TextSubstring(int32_t aStartOffset, int32_t aEndOffset, nsAString& aText); + + /** + * Return text before/at/after the given offset corresponding to + * the boundary type. + */ + void TextBeforeOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText); + void TextAtOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText); + void TextAfterOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText); + + /** + * Return text attributes for the given text range. + */ + already_AddRefed<nsIPersistentProperties> + TextAttributes(bool aIncludeDefAttrs, int32_t aOffset, + int32_t* aStartOffset, int32_t* aEndOffset); + + /** + * Return text attributes applied to the accessible. + */ + already_AddRefed<nsIPersistentProperties> DefaultTextAttributes(); + + /** + * Return text offset of the given child accessible within hypertext + * accessible. + * + * @param aChild [in] accessible child to get text offset for + * @param aInvalidateAfter [in, optional] indicates whether invalidate + * cached offsets for next siblings of the child + */ + int32_t GetChildOffset(const Accessible* aChild, + bool aInvalidateAfter = false) const + { + int32_t index = GetIndexOf(aChild); + return index == -1 ? -1 : GetChildOffset(index, aInvalidateAfter); + } + + /** + * Return text offset for the child accessible index. + */ + int32_t GetChildOffset(uint32_t aChildIndex, + bool aInvalidateAfter = false) const; + + /** + * Return child accessible at the given text offset. + * + * @param aOffset [in] the given text offset + */ + int32_t GetChildIndexAtOffset(uint32_t aOffset) const; + + /** + * Return child accessible at the given text offset. + * + * @param aOffset [in] the given text offset + */ + Accessible* GetChildAtOffset(uint32_t aOffset) const + { + return GetChildAt(GetChildIndexAtOffset(aOffset)); + } + + /** + * Return true if the given offset/range is valid. + */ + bool IsValidOffset(int32_t aOffset); + bool IsValidRange(int32_t aStartOffset, int32_t aEndOffset); + + /** + * Return an offset at the given point. + */ + int32_t OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType); + + /** + * Return a rect of the given text range relative given coordinate system. + */ + nsIntRect TextBounds(int32_t aStartOffset, int32_t aEndOffset, + uint32_t aCoordType = nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE); + + /** + * Return a rect for character at given offset relative given coordinate + * system. + */ + nsIntRect CharBounds(int32_t aOffset, uint32_t aCoordType) + { + 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. + */ + int32_t CaretOffset() const; + void SetCaretOffset(int32_t aOffset); + + /** + * 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 selected regions count within the accessible. + */ + int32_t SelectionCount(); + + /** + * Return the start and end offset of the specified selection. + */ + bool SelectionBoundsAt(int32_t aSelectionNum, + int32_t* aStartOffset, int32_t* aEndOffset); + + /* + * Changes the start and end offset of the specified selection. + * @return true if succeeded + */ + 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 + */ + 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; + + /** + * Return an array of disjoint ranges for selected text within the text control + * or the document this accessible belongs to. + */ + void SelectionRanges(nsTArray<TextRange>* aRanges) const; + + /** + * 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(Accessible* 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 + + void ReplaceText(const nsAString& aText); + void InsertText(const nsAString& aText, int32_t aPosition); + void CopyText(int32_t aStartPos, int32_t aEndPos); + void CutText(int32_t aStartPos, int32_t aEndPos); + void DeleteText(int32_t aStartPos, int32_t aEndPos); + void PasteText(int32_t aPosition); + + /** + * Return the editor associated with the accessible. + */ + virtual already_AddRefed<nsIEditor> GetEditor() const; + + /** + * Return DOM selection object for the accessible. + */ + dom::Selection* DOMSelection() const; + +protected: + virtual ~HyperTextAccessible() { } + + // Accessible + virtual ENameValueFlag NativeName(nsString& aName) override; + + // HyperTextAccessible + + /** + * Transform magic offset into text offset. + */ + index_t ConvertMagicOffset(int32_t aOffset) const; + + /** + * Adjust an offset the caret stays at to get a text by line boundary. + */ + uint32_t AdjustCaretOffset(uint32_t aOffset) const; + + /** + * Return true if caret is at end of line. + */ + bool IsCaretAtEndOfLine() 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) + { + return FindOffset(aOffset, aDirection, eSelectWord, 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); + + /** + * 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 of the substring in case of textual frame or + * frame boundaries in case of non textual frame, offsets are ignored. + */ + nsIntRect 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); + + nsresult SetSelectionRange(int32_t aStartPos, int32_t aEndPos); + + /** + * Convert the given DOM point to a DOM point in non-generated contents. + * + * If aDOMPoint is in ::before, the result is immediately after it. + * If aDOMPoint is in ::after, the result is immediately before it. + * + * @param aDOMPoint [in] A DOM node and an index of its child. This may + * be in a generated content such as ::before or + * ::after. + * @param aElementContent [in] An nsIContent representing an element of + * aDOMPoint.node. + * @return An DOM point which must not be in generated + * contents. + */ + DOMPoint ClosestNotGeneratedDOMPoint(const DOMPoint& aDOMPoint, + nsIContent* aElementContent); + + // Helpers + nsresult GetDOMPointByFrameOffset(nsIFrame* aFrame, int32_t aOffset, + Accessible* 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, int32_t aNodeOffset, + uint32_t* aStartOffset, uint32_t* aEndOffset, + nsIPersistentProperties* aAttributes); + + /** + * Set xml-roles attributes for MathML elements. + * @param aAttributes + */ + void SetMathMLXMLRoles(nsIPersistentProperties* aAttributes); + +private: + /** + * End text offsets array. + */ + mutable nsTArray<uint32_t> mOffsets; +}; + + +//////////////////////////////////////////////////////////////////////////////// +// Accessible downcasting method + +inline HyperTextAccessible* +Accessible::AsHyperText() +{ + return IsHyperText() ? static_cast<HyperTextAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif + diff --git a/accessible/generic/ImageAccessible.cpp b/accessible/generic/ImageAccessible.cpp new file mode 100644 index 0000000000..c6556b04da --- /dev/null +++ b/accessible/generic/ImageAccessible.cpp @@ -0,0 +1,224 @@ +/* -*- 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 "nsAccUtils.h" +#include "Role.h" +#include "AccIterator.h" +#include "States.h" + +#include "imgIContainer.h" +#include "imgIRequest.h" +#include "nsGenericHTMLElement.h" +#include "nsIDocument.h" +#include "nsIImageLoadingContent.h" +#include "nsIPresShell.h" +#include "nsIServiceManager.h" +#include "nsIDOMHTMLImageElement.h" +#include "nsIPersistentProperties2.h" +#include "nsPIDOMWindow.h" +#include "nsIURI.h" + +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// ImageAccessible +//////////////////////////////////////////////////////////////////////////////// + +ImageAccessible:: + ImageAccessible(nsIContent* aContent, DocAccessible* aDoc) : + LinkableAccessible(aContent, aDoc) +{ + mType = eImageType; +} + +ImageAccessible::~ImageAccessible() +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible public + +uint64_t +ImageAccessible::NativeState() +{ + // 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(); + + nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(mContent)); + nsCOMPtr<imgIRequest> imageRequest; + + if (content) + content->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(imageRequest)); + + nsCOMPtr<imgIContainer> imgContainer; + if (imageRequest) + imageRequest->GetImage(getter_AddRefs(imgContainer)); + + if (imgContainer) { + bool animated; + imgContainer->GetAnimated(&animated); + if (animated) + state |= states::ANIMATED; + } + + return state; +} + +ENameValueFlag +ImageAccessible::NativeName(nsString& aName) +{ + bool hasAltAttrib = + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::alt, aName); + if (!aName.IsEmpty()) + return eNameOK; + + ENameValueFlag nameFlag = Accessible::NativeName(aName); + if (!aName.IsEmpty()) + return nameFlag; + + // No accessible name but empty 'alt' attribute is present. If further name + // computation algorithm doesn't provide non empty name then it means + // an empty 'alt' attribute was used to indicate a decorative image (see + // Accessible::Name() method for details). + return hasAltAttrib ? eNoNameOnPurpose : eNameOK; +} + +role +ImageAccessible::NativeRole() +{ + return roles::GRAPHIC; +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible + +uint8_t +ImageAccessible::ActionCount() +{ + 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) +{ + // 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); + + nsIDocument* document = mContent->OwnerDoc(); + nsCOMPtr<nsPIDOMWindowOuter> piWindow = document->GetWindow(); + if (!piWindow) + return false; + + nsCOMPtr<nsPIDOMWindowOuter> tmp; + return NS_SUCCEEDED(piWindow->Open(spec, EmptyString(), EmptyString(), + /* aLoadInfo = */ nullptr, + /* aForceNoOpener = */ false, + getter_AddRefs(tmp))); +} + +//////////////////////////////////////////////////////////////////////////////// +// ImageAccessible + +nsIntPoint +ImageAccessible::Position(uint32_t aCoordType) +{ + nsIntRect rect = Bounds(); + nsAccUtils::ConvertScreenCoordsTo(&rect.x, &rect.y, aCoordType, this); + return rect.TopLeft(); +} + +nsIntSize +ImageAccessible::Size() +{ + return Bounds().Size(); +} + +// Accessible +already_AddRefed<nsIPersistentProperties> +ImageAccessible::NativeAttributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = + LinkableAccessible::NativeAttributes(); + + nsAutoString src; + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src); + if (!src.IsEmpty()) + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::src, src); + + return attributes.forget(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Private methods + +already_AddRefed<nsIURI> +ImageAccessible::GetLongDescURI() const +{ + if (mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::longdesc)) { + // To check if longdesc contains an invalid url. + nsAutoString longdesc; + mContent->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> baseURI = mContent->GetBaseURI(); + nsCOMPtr<nsIURI> uri; + nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), longdesc, + mContent->OwnerDoc(), baseURI); + 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->HasAttr(kNameSpaceID_None, nsGkAtoms::href)) { + nsGenericHTMLElement* element = + nsGenericHTMLElement::FromContent(target); + + nsCOMPtr<nsIURI> uri; + element->GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(uri)); + return uri.forget(); + } + } + } + + return nullptr; +} + +bool +ImageAccessible::IsLongDescIndex(uint8_t aIndex) +{ + return aIndex == LinkableAccessible::ActionCount(); +} + diff --git a/accessible/generic/ImageAccessible.h b/accessible/generic/ImageAccessible.h new file mode 100644 index 0000000000..f2324ae4cd --- /dev/null +++ b/accessible/generic/ImageAccessible.h @@ -0,0 +1,87 @@ +/* -*- 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" + +namespace mozilla { +namespace a11y { + +/* Accessible for supporting images + * supports: + * - gets name, role + * - support basic state + */ +class ImageAccessible : public LinkableAccessible +{ +public: + ImageAccessible(nsIContent* aContent, DocAccessible* aDoc); + + // Accessible + virtual a11y::role NativeRole() override; + virtual uint64_t NativeState() override; + virtual already_AddRefed<nsIPersistentProperties> NativeAttributes() override; + + // ActionAccessible + virtual uint8_t ActionCount() override; + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + virtual bool DoAction(uint8_t aIndex) override; + + // ImageAccessible + nsIntPoint Position(uint32_t aCoordType); + nsIntSize Size(); + +protected: + virtual ~ImageAccessible(); + + // Accessible + virtual ENameValueFlag NativeName(nsString& aName) override; + +private: + /** + * Return whether the element has a longdesc URI. + */ + bool HasLongDesc() const + { + nsCOMPtr<nsIURI> uri = GetLongDescURI(); + return uri; + } + + /** + * 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); + +}; + +//////////////////////////////////////////////////////////////////////////////// +// Accessible downcasting method + +inline ImageAccessible* +Accessible::AsImage() +{ + return IsImage() ? static_cast<ImageAccessible*>(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..a63069355d --- /dev/null +++ b/accessible/generic/OuterDocAccessible.cpp @@ -0,0 +1,218 @@ +/* -*- 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 "Accessible-inl.h" +#include "nsAccUtils.h" +#include "DocAccessible-inl.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/dom/TabParent.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; + + // Request document accessible for the content document to make sure it's + // created. It will appended to outerdoc accessible children asynchronously. + nsIDocument* outerDoc = mContent->GetUncomposedDoc(); + if (outerDoc) { + nsIDocument* innerDoc = outerDoc->GetSubDocumentFor(mContent); + if (innerDoc) + GetAccService()->GetDocAccessible(innerDoc); + } +} + +OuterDocAccessible::~OuterDocAccessible() +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_ISUPPORTS_INHERITED0(OuterDocAccessible, + Accessible) + +//////////////////////////////////////////////////////////////////////////////// +// Accessible public (DON'T add methods here) + +role +OuterDocAccessible::NativeRole() +{ + return roles::INTERNAL_FRAME; +} + +Accessible* +OuterDocAccessible::ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) +{ + nsIntRect docRect = Bounds(); + if (aX < docRect.x || aX >= docRect.x + docRect.width || + aY < docRect.y || aY >= docRect.y + docRect.height) + return nullptr; + + // Always return the inner doc as direct child accessible unless bounds + // outside of it. + Accessible* child = GetChildAt(0); + NS_ENSURE_TRUE(child, nullptr); + + if (aWhichChild == eDeepestChild) + return child->ChildAtPoint(aX, aY, eDeepestChild); + return child; +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible public + +void +OuterDocAccessible::Shutdown() +{ +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) + logging::OuterDocDestroy(this); +#endif + + Accessible* 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()) { + mDoc->BindChildDocument(child->AsDoc()); + } + } + + AccessibleWrap::Shutdown(); +} + +bool +OuterDocAccessible::InsertChildAt(uint32_t aIdx, Accessible* 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(Accessible* aAccessible) +{ + Accessible* child = mChildren.SafeElementAt(0, nullptr); + if (child != aAccessible) { + NS_ERROR("Wrong child to remove!"); + 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; +} + +#if defined(XP_WIN) + +// On Windows e10s, since we don't cache in the chrome process, these next two +// functions must be implemented so that we properly cross the chrome-to-content +// boundary when traversing. + +uint32_t +OuterDocAccessible::ChildCount() const +{ + uint32_t result = mChildren.Length(); + if (!result && RemoteChildDoc()) { + result = 1; + } + return result; +} + +Accessible* +OuterDocAccessible::GetChildAt(uint32_t aIndex) const +{ + Accessible* result = AccessibleWrap::GetChildAt(aIndex); + if (result || aIndex) { + return result; + } + // If we are asking for child 0 and GetChildAt doesn't return anything, try + // to get the remote child doc and return that instead. + ProxyAccessible* remoteChild = RemoteChildDoc(); + if (!remoteChild) { + return nullptr; + } + return WrapperFor(remoteChild); +} + +#endif // defined(XP_WIN) + +ProxyAccessible* +OuterDocAccessible::RemoteChildDoc() const +{ + dom::TabParent* tab = dom::TabParent::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..bfa02ba14c --- /dev/null +++ b/accessible/generic/OuterDocAccessible.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_A11Y_OUTERDOCACCESSIBLE_H_ +#define MOZILLA_A11Y_OUTERDOCACCESSIBLE_H_ + +#include "AccessibleWrap.h" + +namespace mozilla { +namespace a11y { +class ProxyAccessible; + +/** + * 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_DECL_ISUPPORTS_INHERITED + + ProxyAccessible* RemoteChildDoc() const; + + // Accessible + virtual void Shutdown() override; + virtual mozilla::a11y::role NativeRole() override; + virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) override; + + virtual bool InsertChildAt(uint32_t aIdx, Accessible* aChild) override; + virtual bool RemoveChild(Accessible* aAccessible) override; + virtual bool IsAcceptableChild(nsIContent* aEl) const override; + +#if defined(XP_WIN) + virtual uint32_t ChildCount() const override; + virtual Accessible* GetChildAt(uint32_t aIndex) const override; +#endif // defined(XP_WIN) + +protected: + virtual ~OuterDocAccessible() override; +}; + +inline OuterDocAccessible* +Accessible::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..5817f2da9c --- /dev/null +++ b/accessible/generic/RootAccessible.cpp @@ -0,0 +1,732 @@ +/* -*- 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" + +#define CreateEvent CreateEventA +#include "nsIDOMDocument.h" + +#include "Accessible-inl.h" +#include "DocAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "nsEventShell.h" +#include "Relation.h" +#include "Role.h" +#include "States.h" +#ifdef MOZ_XUL +#include "XULTreeAccessible.h" +#endif + +#include "mozilla/dom/Element.h" + +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "nsIDOMCustomEvent.h" +#include "nsIDOMXULMultSelectCntrlEl.h" +#include "nsIDocument.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPropertyBag2.h" +#include "nsIServiceManager.h" +#include "nsPIDOMWindow.h" +#include "nsIWebBrowserChrome.h" +#include "nsReadableUtils.h" +#include "nsFocusManager.h" +#include "nsGlobalWindow.h" + +#ifdef MOZ_XUL +#include "nsIXULDocument.h" +#include "nsIXULWindow.h" +#endif + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_ISUPPORTS_INHERITED0(RootAccessible, DocAccessible) + +//////////////////////////////////////////////////////////////////////////////// +// Constructor/destructor + +RootAccessible:: + RootAccessible(nsIDocument* aDocument, nsIPresShell* aPresShell) : + DocAccessibleWrap(aDocument, aPresShell) +{ + mType = eRootType; +} + +RootAccessible::~RootAccessible() +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible + +ENameValueFlag +RootAccessible::Name(nsString& aName) +{ + aName.Truncate(); + + if (ARIARoleMap()) { + Accessible::Name(aName); + if (!aName.IsEmpty()) + return eNameOK; + } + + mDocumentNode->GetTitle(aName); + return eNameOK; +} + +role +RootAccessible::NativeRole() +{ + // If it's a <dialog> or <wizard>, use roles::DIALOG instead + dom::Element* rootElm = mDocumentNode->GetRootElement(); + if (rootElm && rootElm->IsAnyOfXULElements(nsGkAtoms::dialog, + nsGkAtoms::wizard)) + return roles::DIALOG; + + return DocAccessibleWrap::NativeRole(); +} + +// RootAccessible protected member +#ifdef MOZ_XUL +uint32_t +RootAccessible::GetChromeFlags() +{ + // Return the flag set for the top level window as defined + // by nsIWebBrowserChrome::CHROME_WINDOW_[FLAGNAME] + // Not simple: nsIXULWindow 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<nsIXULWindow> xulWin(do_GetInterface(treeOwner)); + if (!xulWin) { + return 0; + } + uint32_t chromeFlags; + xulWin->GetChromeFlags(&chromeFlags); + return chromeFlags; +} +#endif + +uint64_t +RootAccessible::NativeState() +{ + uint64_t state = DocAccessibleWrap::NativeState(); + if (state & states::DEFUNCT) + return state; + +#ifdef MOZ_XUL + 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; +#endif + + 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.xml) + "RadioStateChange", + "popupshown", + "popuphiding", + "DOMMenuInactive", + "DOMMenuItemActive", + "DOMMenuItemInactive", + "DOMMenuBarActive", + "DOMMenuBarInactive" +}; + +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. + nsCOMPtr<EventTarget> nstarget = mDocumentNode; + + 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, 2); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return DocAccessible::AddEventListeners(); +} + +nsresult +RootAccessible::RemoveEventListeners() +{ + nsCOMPtr<EventTarget> target = mDocumentNode; + if (target) { + for (const char* const* e = kEventTypes, + * const* e_end = ArrayEnd(kEventTypes); + e < e_end; ++e) { + nsresult rv = target->RemoveEventListener(NS_ConvertASCIItoUTF16(*e), this, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // 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(nsIDOMEvent* aDOMEvent) +{ + MOZ_ASSERT(aDOMEvent); + Event* event = aDOMEvent->InternalDOMEvent(); + nsCOMPtr<nsINode> origTargetNode = do_QueryInterface(event->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) { + // Root accessible exists longer than any of its descendant documents so + // that we are guaranteed notification is processed before root accessible + // is destroyed. + document->HandleNotification<RootAccessible, nsIDOMEvent> + (this, &RootAccessible::ProcessDOMEvent, aDOMEvent); + } + + return NS_OK; +} + +// RootAccessible protected +void +RootAccessible::ProcessDOMEvent(nsIDOMEvent* aDOMEvent) +{ + MOZ_ASSERT(aDOMEvent); + Event* event = aDOMEvent->InternalDOMEvent(); + nsCOMPtr<nsINode> origTargetNode = do_QueryInterface(event->GetOriginalTarget()); + + nsAutoString eventType; + aDOMEvent->GetType(eventType); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDOMEvents)) + logging::DOMEvent("processed", origTargetNode, eventType); +#endif + + if (eventType.EqualsLiteral("popuphiding")) { + HandlePopupHidingEvent(origTargetNode); + return; + } + + DocAccessible* targetDocument = GetAccService()-> + GetDocAccessible(origTargetNode->OwnerDoc()); + NS_ASSERTION(targetDocument, "No document while accessible is in document?!"); + + Accessible* accessible = + targetDocument->GetAccessibleOrContainer(origTargetNode); + if (!accessible) + return; + +#ifdef MOZ_XUL + XULTreeAccessible* treeAcc = accessible->AsXULTree(); + if (treeAcc) { + if (eventType.EqualsLiteral("TreeRowCountChanged")) { + HandleTreeRowCountChangedEvent(aDOMEvent, treeAcc); + return; + } + + if (eventType.EqualsLiteral("TreeInvalidated")) { + HandleTreeInvalidatedEvent(aDOMEvent, treeAcc); + return; + } + } +#endif + + 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; + } + + Accessible* treeItemAcc = nullptr; +#ifdef MOZ_XUL + // 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 = + do_QueryInterface(targetNode); + 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 +#endif + 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); + } + } + else if (eventType.EqualsLiteral("DOMMenuItemActive")) { + FocusMgr()->ActiveItemChanged(accessible); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::ActiveItemChangeCausedBy("DOMMenuItemActive", accessible); +#endif + } + else if (eventType.EqualsLiteral("DOMMenuItemInactive")) { + // 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. + Accessible* 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. + Accessible* 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 +} + + +//////////////////////////////////////////////////////////////////////////////// +// Accessible + +void +RootAccessible::Shutdown() +{ + // Called manually or by Accessible::LastRelease() + if (!PresShell()) + return; // Already shutdown + + DocAccessibleWrap::Shutdown(); +} + +Relation +RootAccessible::RelationByType(RelationType aType) +{ + if (!mDocumentNode || aType != RelationType::EMBEDS) + return DocAccessibleWrap::RelationByType(aType); + + if (nsPIDOMWindowOuter* rootWindow = mDocumentNode->GetWindow()) { + nsCOMPtr<nsPIDOMWindowOuter> contentWindow = nsGlobalWindow::Cast(rootWindow)->GetContent(); + if (contentWindow) { + nsCOMPtr<nsIDocument> contentDocumentNode = contentWindow->GetDoc(); + if (contentDocumentNode) { + DocAccessible* contentDocument = + GetAccService()->GetDocAccessible(contentDocumentNode); + if (contentDocument) + return Relation(contentDocument); + } + } + } + + return Relation(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Protected members + +void +RootAccessible::HandlePopupShownEvent(Accessible* 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::TOOLTIP) { + // There is a single <xul:tooltip> node which Mozilla moves around. + // The accessible for it stays the same no matter where it moves. + // AT's expect to get an EVENT_SHOW for the tooltip. + // In event callback the tooltip's accessible will be ready. + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SHOW, aAccessible); + return; + } + + if (role == roles::COMBOBOX_LIST) { + // Fire expanded state change event for comboboxes and autocompeletes. + Accessible* combobox = aAccessible->Parent(); + if (!combobox) + return; + + roles::Role comboboxRole = combobox->Role(); + if (comboboxRole == roles::COMBOBOX || + comboboxRole == roles::AUTOCOMPLETE) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(combobox, states::EXPANDED, true); + if (event) + nsEventShell::FireEvent(event); + } + } +} + +void +RootAccessible::HandlePopupHidingEvent(nsINode* aPopupNode) +{ + // 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. + DocAccessible* document = nsAccUtils::GetDocAccessibleFor(aPopupNode); + if (!document) + return; + + Accessible* popup = document->GetAccessible(aPopupNode); + if (!popup) { + Accessible* popupContainer = document->GetContainerAccessible(aPopupNode); + if (!popupContainer) + return; + + uint32_t childCount = popupContainer->ChildCount(); + for (uint32_t idx = 0; idx < childCount; idx++) { + Accessible* child = popupContainer->GetChildAt(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. + // When popup closes (except nested popups and menus) then fire focus event to + // where it was. The focus event is expected even if popup didn't take a focus. + + static const uint32_t kNotifyOfFocus = 1; + static const uint32_t kNotifyOfState = 2; + uint32_t notifyOf = 0; + + // 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. + Accessible* widget = nullptr; + if (popup->IsCombobox()) { + widget = popup; + } else { + widget = popup->ContainerWidget(); + if (!widget) { + if (!popup->IsMenuPopup()) + return; + + widget = popup; + } + } + + if (popup->IsAutoCompletePopup()) { + // No focus event for autocomplete because it's managed by + // DOMMenuItemInactive events. + if (widget->IsAutoComplete()) + notifyOf = kNotifyOfState; + + } else if (widget->IsCombobox()) { + // Fire focus for active combobox, otherwise the focus is managed by DOM + // focus notifications. Always fire state change event. + if (widget->IsActiveWidget()) + notifyOf = kNotifyOfFocus; + notifyOf |= kNotifyOfState; + + } else if (widget->IsMenuButton()) { + // Can be a part of autocomplete. + Accessible* compositeWidget = widget->ContainerWidget(); + if (compositeWidget && compositeWidget->IsAutoComplete()) { + widget = compositeWidget; + notifyOf = kNotifyOfState; + } + + // Autocomplete (like searchbar) can be inactive when popup hiddens + notifyOf |= kNotifyOfFocus; + + } else if (widget == popup) { + // Top level context menus and alerts. + // Ignore submenus and menubar. When submenu is closed then sumbenu + // container menuitem takes a focus via DOMMenuItemActive notification. + // For menubars processing we listen DOMMenubarActive/Inactive + // notifications. + notifyOf = kNotifyOfFocus; + } + + // Restore focus to where it was. + if (notifyOf & kNotifyOfFocus) { + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::ActiveItemChangeCausedBy("popuphiding", popup); +#endif + } + + // Fire expanded state change event. + if (notifyOf & kNotifyOfState) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(widget, states::EXPANDED, false); + document->FireDelayedEvent(event); + } +} + +#ifdef MOZ_XUL +void +RootAccessible::HandleTreeRowCountChangedEvent(nsIDOMEvent* aEvent, + XULTreeAccessible* aAccessible) +{ + nsCOMPtr<nsIDOMCustomEvent> customEvent(do_QueryInterface(aEvent)); + if (!customEvent) + return; + + nsCOMPtr<nsIVariant> detailVariant; + customEvent->GetDetail(getter_AddRefs(detailVariant)); + if (!detailVariant) + return; + + nsCOMPtr<nsISupports> supports; + detailVariant->GetAsISupports(getter_AddRefs(supports)); + nsCOMPtr<nsIPropertyBag2> propBag(do_QueryInterface(supports)); + if (!propBag) + return; + + nsresult rv; + int32_t index, count; + rv = propBag->GetPropertyAsInt32(NS_LITERAL_STRING("index"), &index); + if (NS_FAILED(rv)) + return; + + rv = propBag->GetPropertyAsInt32(NS_LITERAL_STRING("count"), &count); + if (NS_FAILED(rv)) + return; + + aAccessible->InvalidateCache(index, count); +} + +void +RootAccessible::HandleTreeInvalidatedEvent(nsIDOMEvent* aEvent, + XULTreeAccessible* aAccessible) +{ + nsCOMPtr<nsIDOMCustomEvent> customEvent(do_QueryInterface(aEvent)); + if (!customEvent) + return; + + nsCOMPtr<nsIVariant> detailVariant; + customEvent->GetDetail(getter_AddRefs(detailVariant)); + if (!detailVariant) + return; + + nsCOMPtr<nsISupports> supports; + detailVariant->GetAsISupports(getter_AddRefs(supports)); + nsCOMPtr<nsIPropertyBag2> propBag(do_QueryInterface(supports)); + if (!propBag) + return; + + int32_t startRow = 0, endRow = -1, startCol = 0, endCol = -1; + propBag->GetPropertyAsInt32(NS_LITERAL_STRING("startrow"), + &startRow); + propBag->GetPropertyAsInt32(NS_LITERAL_STRING("endrow"), + &endRow); + propBag->GetPropertyAsInt32(NS_LITERAL_STRING("startcolumn"), + &startCol); + propBag->GetPropertyAsInt32(NS_LITERAL_STRING("endcolumn"), + &endCol); + + aAccessible->TreeViewInvalidated(startRow, endRow, startCol, endCol); +} +#endif + +ProxyAccessible* +RootAccessible::GetPrimaryRemoteTopLevelContentDoc() const +{ + nsCOMPtr<nsIDocShellTreeOwner> owner; + mDocumentNode->GetDocShell()->GetTreeOwner(getter_AddRefs(owner)); + NS_ENSURE_TRUE(owner, nullptr); + + nsCOMPtr<nsITabParent> tabParent; + owner->GetPrimaryTabParent(getter_AddRefs(tabParent)); + if (!tabParent) { + return nullptr; + } + + auto tab = static_cast<dom::TabParent*>(tabParent.get()); + return tab->GetTopLevelDocAccessible(); +} diff --git a/accessible/generic/RootAccessible.h b/accessible/generic/RootAccessible.h new file mode 100644 index 0000000000..beb74cf4b6 --- /dev/null +++ b/accessible/generic/RootAccessible.h @@ -0,0 +1,92 @@ +/* -*- 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" + +class nsIDocument; + +namespace mozilla { +namespace a11y { + +class RootAccessible : public DocAccessibleWrap, + public nsIDOMEventListener +{ + NS_DECL_ISUPPORTS_INHERITED + +public: + RootAccessible(nsIDocument* aDocument, nsIPresShell* aPresShell); + + // nsIDOMEventListener + NS_IMETHOD HandleEvent(nsIDOMEvent* aEvent) override; + + // Accessible + virtual void Shutdown() override; + virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) override; + virtual Relation RelationByType(RelationType aType) override; + virtual mozilla::a11y::role NativeRole() override; + virtual uint64_t NativeState() override; + + // RootAccessible + + /** + * Notify that the sub document presshell was activated. + */ + virtual void DocumentActivated(DocAccessible* aDocument); + + /** + * Return the primary remote top level document if any. + */ + ProxyAccessible* GetPrimaryRemoteTopLevelContentDoc() const; + +protected: + virtual ~RootAccessible(); + + /** + * Add/remove DOM event listeners. + */ + virtual nsresult AddEventListeners() override; + virtual nsresult RemoveEventListeners() override; + + /** + * Process the DOM event. + */ + void ProcessDOMEvent(nsIDOMEvent* aEvent); + + /** + * Process "popupshown" event. Used by HandleEvent(). + */ + void HandlePopupShownEvent(Accessible* aAccessible); + + /* + * Process "popuphiding" event. Used by HandleEvent(). + */ + void HandlePopupHidingEvent(nsINode* aNode); + +#ifdef MOZ_XUL + void HandleTreeRowCountChangedEvent(nsIDOMEvent* aEvent, + XULTreeAccessible* aAccessible); + void HandleTreeInvalidatedEvent(nsIDOMEvent* aEvent, + XULTreeAccessible* aAccessible); + + uint32_t GetChromeFlags(); +#endif +}; + +inline RootAccessible* +Accessible::AsRoot() +{ + return IsRoot() ? static_cast<mozilla::a11y::RootAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/TableAccessible.h b/accessible/generic/TableAccessible.h new file mode 100644 index 0000000000..fbb8393b67 --- /dev/null +++ b/accessible/generic/TableAccessible.h @@ -0,0 +1,187 @@ +/* -*- 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 "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +namespace a11y { + +class Accessible; + +/** + * Accessible table interface. + */ +class TableAccessible +{ +public: + + /** + * Return the caption accessible if any for this table. + */ + virtual Accessible* Caption() const { return nullptr; } + + /** + * Get the summary for this table. + */ + virtual void Summary(nsString& aSummary) { aSummary.Truncate(); } + + /** + * Return the number of columns in the table. + */ + virtual uint32_t ColCount() { return 0; } + + /** + * Return the number of rows in the table. + */ + virtual uint32_t RowCount() { return 0; } + + /** + * Return the accessible for the cell at the given row and column indices. + */ + virtual Accessible* CellAt(uint32_t aRowIdx, uint32_t aColIdx) { return nullptr; } + + /** + * Return the index of the cell at the given row and column. + */ + virtual int32_t CellIndexAt(uint32_t aRowIdx, uint32_t aColIdx) + { return ColCount() * aRowIdx + aColIdx; } + + /** + * Return the column index of the cell with the given index. + */ + virtual int32_t ColIndexAt(uint32_t aCellIdx) + { return aCellIdx % ColCount(); } + + /** + * Return the row index of the cell with the given index. + */ + virtual int32_t RowIndexAt(uint32_t aCellIdx) + { return aCellIdx / ColCount(); } + + /** + * Get the row and column indices for the cell at the given index. + */ + virtual void RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx, + int32_t* aColIdx) + { + uint32_t colCount = ColCount(); + *aRowIdx = aCellIdx / colCount; + *aColIdx = aCellIdx % colCount; + } + + /** + * Return the number of columns occupied by the cell at the given row and + * column indices. + */ + virtual uint32_t ColExtentAt(uint32_t aRowIdx, uint32_t aColIdx) { return 1; } + + /** + * Return the number of rows occupied by the cell at the given row and column + * indices. + */ + virtual uint32_t RowExtentAt(uint32_t aRowIdx, uint32_t aColIdx) { return 1; } + + /** + * Get the description of the given column. + */ + virtual void ColDescription(uint32_t aColIdx, nsString& aDescription) + { aDescription.Truncate(); } + + /** + * Get the description for the given row. + */ + virtual void RowDescription(uint32_t aRowIdx, nsString& aDescription) + { aDescription.Truncate(); } + + /** + * Return true if the given column is selected. + */ + virtual bool IsColSelected(uint32_t aColIdx) { return false; } + + /** + * Return true if the given row is selected. + */ + virtual bool IsRowSelected(uint32_t aRowIdx) { return false; } + + /** + * Return true if the given cell is selected. + */ + virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) { return false; } + + /** + * Return the number of selected cells. + */ + virtual uint32_t SelectedCellCount() { return 0; } + + /** + * Return the number of selected columns. + */ + virtual uint32_t SelectedColCount() { return 0; } + + /** + * Return the number of selected rows. + */ + virtual uint32_t SelectedRowCount() { return 0; } + + /** + * Get the set of selected cells. + */ + virtual void SelectedCells(nsTArray<Accessible*>* aCells) = 0; + + /** + * Get the set of selected cell indices. + */ + virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) = 0; + + /** + * Get the set of selected column indices. + */ + virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) = 0; + + /** + * Get the set of selected row indices. + */ + virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) = 0; + + /** + * Select the given column unselecting any other selected columns. + */ + virtual void SelectCol(uint32_t aColIdx) {} + + /** + * Select the given row unselecting all other previously selected rows. + */ + virtual void SelectRow(uint32_t aRowIdx) {} + + /** + * Unselect the given column leaving other selected columns selected. + */ + virtual void UnselectCol(uint32_t aColIdx) {} + + /** + * Unselect the given row leaving other selected rows selected. + */ + virtual void UnselectRow(uint32_t aRowIdx) {} + + /** + * Return true if the table is probably for layout. + */ + virtual bool IsProbablyLayoutTable() { return false; } + + /** + * Convert the table to an Accessible*. + */ + virtual Accessible* AsAccessible() = 0; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/TableCellAccessible.cpp b/accessible/generic/TableCellAccessible.cpp new file mode 100644 index 0000000000..3c840127cb --- /dev/null +++ b/accessible/generic/TableCellAccessible.cpp @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TableCellAccessible.h" + +#include "Accessible-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--) { + Accessible* 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); + } +} + +void +TableCellAccessible::ColHeaderCells(nsTArray<Accessible*>* aCells) +{ + uint32_t rowIdx = RowIdx(), colIdx = ColIdx(); + TableAccessible* table = Table(); + if (!table) + return; + + // Move up to find column header cells + for (uint32_t curRowIdx = rowIdx - 1; curRowIdx < rowIdx; curRowIdx--) { + Accessible* cell = table->CellAt(curRowIdx, colIdx); + 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 rows + // we'll get it later. + if (tableCell->RowIdx() == curRowIdx && cell->Role() == roles::COLUMNHEADER) + aCells->AppendElement(cell); + } +} diff --git a/accessible/generic/TableCellAccessible.h b/accessible/generic/TableCellAccessible.h new file mode 100644 index 0000000000..327552fe82 --- /dev/null +++ b/accessible/generic/TableCellAccessible.h @@ -0,0 +1,70 @@ +/* -*- 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 "nsTArray.h" +#include <stdint.h> + +namespace mozilla { +namespace a11y { + +class Accessible; +class TableAccessible; + +/** + * Abstract interface implemented by table cell accessibles. + */ +class TableCellAccessible +{ +public: + + /** + * Return the table this cell is in. + */ + virtual TableAccessible* Table() const = 0; + + /** + * Return the column of the table this cell is in. + */ + virtual uint32_t ColIdx() const = 0; + + /** + * Return the row of the table this cell is in. + */ + virtual uint32_t RowIdx() const = 0; + + /** + * Return the column extent of this cell. + */ + virtual uint32_t ColExtent() const { return 1; } + + /** + * Return the row extent of this cell. + */ + virtual uint32_t RowExtent() const { return 1; } + + /** + * Return the column header cells for this cell. + */ + virtual void ColHeaderCells(nsTArray<Accessible*>* aCells); + + /** + * Return the row header cells for this cell. + */ + virtual void RowHeaderCells(nsTArray<Accessible*>* aCells); + + /** + * Returns true if this cell is selected. + */ + virtual bool Selected() = 0; +}; + +} // 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..9808833e10 --- /dev/null +++ b/accessible/generic/TextLeafAccessible.cpp @@ -0,0 +1,54 @@ +/* -*- 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() +{ + 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) +{ + // 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..555929fbf2 --- /dev/null +++ b/accessible/generic/TextLeafAccessible.h @@ -0,0 +1,51 @@ +/* -*- 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(); + + // Accessible + virtual mozilla::a11y::role NativeRole() override; + virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0, + uint32_t aLength = UINT32_MAX) override; + virtual ENameValueFlag Name(nsString& aName) override; + + // TextLeafAccessible + void SetText(const nsAString& aText) { mText = aText; } + const nsString& Text() const { return mText; } + +protected: + nsString mText; +}; + + +//////////////////////////////////////////////////////////////////////////////// +// Accessible downcast method + +inline TextLeafAccessible* +Accessible::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..a9e97acf04 --- /dev/null +++ b/accessible/generic/moz.build @@ -0,0 +1,70 @@ +# -*- 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 += [ + 'Accessible.h', + 'DocAccessible.h', + 'HyperTextAccessible.h', +] + +UNIFIED_SOURCES += [ + 'Accessible.cpp', + 'ApplicationAccessible.cpp', + 'ARIAGridAccessible.cpp', + 'BaseAccessibles.cpp', + 'DocAccessible.cpp', + 'FormControlAccessible.cpp', + 'HyperTextAccessible.cpp', + 'ImageAccessible.cpp', + 'OuterDocAccessible.cpp', + 'RootAccessible.cpp', + 'TableCellAccessible.cpp', + 'TextLeafAccessible.cpp', +] + +LOCAL_INCLUDES += [ + '/accessible/base', + '/accessible/html', + '/accessible/xpcom', + '/accessible/xul', + '/dom/base', + '/layout/generic', + '/layout/xul', +] + +if CONFIG['OS_ARCH'] == 'WINNT': + LOCAL_INCLUDES += [ + '/accessible/ipc/win', + ] +else: + LOCAL_INCLUDES += [ + '/accessible/ipc/other', + ] + +if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + 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', + ] +else: + LOCAL_INCLUDES += [ + '/accessible/other', + ] + +FINAL_LIBRARY = 'xul' + +include('/ipc/chromium/chromium-config.mozbuild') + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] |