| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <optional> |
| |
| #include "fuchsia/accessibility/semantics/cpp/fidl.h" |
| // This header file has been generated from the strings library fuchsia.intl.l10n. |
| #include "fuchsia/intl/l10n/cpp/fidl.h" |
| #include "src/lib/fxl/strings/trim.h" |
| #include "src/ui/a11y/lib/screen_reader/screen_reader_message_generator.h" |
| #include "src/ui/a11y/lib/screen_reader/util/util.h" |
| |
| namespace a11y { |
| namespace { |
| |
| using fuchsia::accessibility::semantics::Node; |
| using fuchsia::accessibility::semantics::Role; |
| using fuchsia::accessibility::tts::Utterance; |
| using fuchsia::intl::l10n::MessageIds; |
| |
| static constexpr zx::duration kDefaultDelay = zx::msec(40); |
| static constexpr zx::duration kLongDelay = zx::msec(100); |
| |
| // Currently, this just checks if the node has a DEFAULT action. |
| // |
| // todo(https://fxbug.dev/42057890): implement better handling for secondary actions. |
| bool NodeIsClickable(const Node* node) { |
| if (!node->has_actions()) { |
| return false; |
| } |
| for (const auto& action : node->actions()) { |
| if (action == fuchsia::accessibility::semantics::Action::DEFAULT) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| std::optional<MessageIds> RoleToMessageId(Role role) { |
| switch (role) { |
| case Role::HEADER: |
| return MessageIds::ROLE_HEADER; |
| case Role::IMAGE: |
| return MessageIds::ROLE_IMAGE; |
| case Role::LINK: |
| return MessageIds::ROLE_LINK; |
| case Role::TEXT_FIELD: |
| return MessageIds::ROLE_TEXT_FIELD; |
| case Role::SEARCH_BOX: |
| return MessageIds::ROLE_SEARCH_BOX; |
| case Role::SLIDER: |
| return MessageIds::ROLE_SLIDER; |
| default: |
| return std::nullopt; |
| } |
| } |
| |
| } // namespace |
| |
| ScreenReaderMessageGenerator::ScreenReaderMessageGenerator( |
| std::unique_ptr<i18n::MessageFormatter> message_formatter) |
| : message_formatter_(std::move(message_formatter)) { |
| character_to_message_id_.insert({"!", MessageIds::EXCLAMATION_SYMBOL_NAME}); |
| character_to_message_id_.insert({"?", MessageIds::QUESTION_MARK_SYMBOL_NAME}); |
| character_to_message_id_.insert({"_", MessageIds::UNDERSCORE_SYMBOL_NAME}); |
| character_to_message_id_.insert({"/", MessageIds::FORWARD_SLASH_SYMBOL_NAME}); |
| character_to_message_id_.insert({",", MessageIds::COMMA_SYMBOL_NAME}); |
| character_to_message_id_.insert({".", MessageIds::PERIOD_SYMBOL_NAME}); |
| character_to_message_id_.insert({"<", MessageIds::LESS_THAN_SYMBOL_NAME}); |
| character_to_message_id_.insert({">", MessageIds::GREATER_THAN_SYMBOL_NAME}); |
| character_to_message_id_.insert({"@", MessageIds::AT_SYMBOL_NAME}); |
| character_to_message_id_.insert({"#", MessageIds::POUND_SYMBOL_NAME}); |
| character_to_message_id_.insert({"$", MessageIds::DOLLAR_SYMBOL_NAME}); |
| character_to_message_id_.insert({"%", MessageIds::PERCENT_SYMBOL_NAME}); |
| character_to_message_id_.insert({"&", MessageIds::AMPERSAND_SYMBOL_NAME}); |
| character_to_message_id_.insert({"-", MessageIds::DASH_SYMBOL_NAME}); |
| character_to_message_id_.insert({"+", MessageIds::PLUS_SYMBOL_NAME}); |
| character_to_message_id_.insert({"=", MessageIds::EQUALS_SYMBOL_NAME}); |
| character_to_message_id_.insert({"(", MessageIds::LEFT_PARENTHESIS_SYMBOL_NAME}); |
| character_to_message_id_.insert({")", MessageIds::RIGHT_PARENTHESIS_SYMBOL_NAME}); |
| character_to_message_id_.insert({"\\", MessageIds::BACKSLASH_SYMBOL_NAME}); |
| character_to_message_id_.insert({"*", MessageIds::ASTERISK_SYMBOL_NAME}); |
| character_to_message_id_.insert({"\"", MessageIds::DOUBLE_QUOTATION_MARK_SYMBOL_NAME}); |
| character_to_message_id_.insert({"'", MessageIds::SINGLE_QUOTATION_MARK_SYMBOL_NAME}); |
| character_to_message_id_.insert({":", MessageIds::COLON_SYMBOL_NAME}); |
| character_to_message_id_.insert({";", MessageIds::SEMICOLON_SYMBOL_NAME}); |
| character_to_message_id_.insert({"~", MessageIds::TILDE_SYMBOL_NAME}); |
| character_to_message_id_.insert({"`", MessageIds::GRAVE_ACCENT_SYMBOL_NAME}); |
| character_to_message_id_.insert({"|", MessageIds::VERTICAL_LINE_SYMBOL_NAME}); |
| character_to_message_id_.insert({"√", MessageIds::SQUARE_ROOT_SYMBOL_NAME}); |
| character_to_message_id_.insert({"•", MessageIds::BULLET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"◦", MessageIds::WHITE_BULLET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"▪", MessageIds::BLACK_SQUARE_SYMBOL_NAME}); |
| character_to_message_id_.insert({"‣", MessageIds::TRIANGULAR_BULLET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"⁃", MessageIds::HYPHEN_BULLET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"✕", MessageIds::MULTIPLICATION_SYMBOL_NAME}); |
| character_to_message_id_.insert({"÷", MessageIds::DIVISION_SYMBOL_NAME}); |
| character_to_message_id_.insert({"¶", MessageIds::PILCROW_SYMBOL_NAME}); |
| character_to_message_id_.insert({"π", MessageIds::PI_SYMBOL_NAME}); |
| character_to_message_id_.insert({"∆", MessageIds::DELTA_SYMBOL_NAME}); |
| character_to_message_id_.insert({"£", MessageIds::BRITISH_POUND_SYMBOL_NAME}); |
| character_to_message_id_.insert({"¢", MessageIds::CENT_SYMBOL_NAME}); |
| character_to_message_id_.insert({"€", MessageIds::EURO_SYMBOL_NAME}); |
| character_to_message_id_.insert({"¥", MessageIds::YEN_SYMBOL_NAME}); |
| character_to_message_id_.insert({"^", MessageIds::CARET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"°", MessageIds::DEGREE_SYMBOL_NAME}); |
| character_to_message_id_.insert({"{", MessageIds::LEFT_CURLY_BRACKET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"}", MessageIds::RIGHT_CURLY_BRACKET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"©", MessageIds::COPYRIGHT_SYMBOL_NAME}); |
| character_to_message_id_.insert({"®", MessageIds::REGISTERED_TRADEMARK_SYMBOL_NAME}); |
| character_to_message_id_.insert({"™", MessageIds::TRADEMARK_SYMBOL_NAME}); |
| character_to_message_id_.insert({"[", MessageIds::LEFT_SQUARE_BRACKET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"]", MessageIds::RIGHT_SQUARE_BRACKET_SYMBOL_NAME}); |
| character_to_message_id_.insert({"¡", MessageIds::INVERTED_EXCLAMATION_POINT_SYMBOL_NAME}); |
| character_to_message_id_.insert({"¿", MessageIds::INVERTED_QUESTION_MARK_SYMBOL_NAME}); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeContainerChanges( |
| const ScreenReaderMessageContext& message_context, |
| std::vector<UtteranceAndContext>& description) { |
| // Give hints for exited containers. |
| for (auto& container : message_context.exited_containers) { |
| if (container.has_role() && container.role() == Role::TABLE) { |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::EXITED_TABLE)); |
| } else if (container.has_role() && container.role() == Role::LIST) { |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::EXITED_LIST)); |
| } |
| } |
| |
| // Give hints for entered containers. |
| for (auto& container : message_context.entered_containers) { |
| if (container.has_role() && container.role() == Role::TABLE) { |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ENTERED_TABLE)); |
| DescribeTable(container, description); |
| } else if (container.has_role() && container.role() == Role::LIST) { |
| DescribeEnteredList(container, description); |
| } |
| } |
| } |
| |
| std::vector<ScreenReaderMessageGenerator::UtteranceAndContext> |
| ScreenReaderMessageGenerator::DescribeNode(const Node* node, |
| ScreenReaderMessageContext message_context) { |
| std::vector<UtteranceAndContext> description; |
| |
| DescribeContainerChanges(message_context, description); |
| |
| auto role = node->has_role() ? node->role() : Role::UNKNOWN; |
| |
| if (role == Role::UNKNOWN && NodeIsSlider(node)) { |
| role = Role::SLIDER; |
| } |
| |
| switch (role) { |
| case Role::BUTTON: |
| DescribeButton(node, description); |
| break; |
| case Role::RADIO_BUTTON: |
| DescribeRadioButton(node, description); |
| break; |
| case Role::TOGGLE_SWITCH: |
| DescribeToggleSwitch(node, description); |
| break; |
| case Role::SLIDER: |
| DescribeSlider(node, description); |
| break; |
| case Role::ROW_HEADER: |
| case Role::COLUMN_HEADER: |
| DescribeRowOrColumnHeader(node, description); |
| break; |
| case Role::CELL: |
| DescribeTableCell(node, std::move(message_context), description); |
| break; |
| case Role::CHECK_BOX: |
| DescribeCheckBox(node, description); |
| break; |
| case Role::LIST_ELEMENT_MARKER: |
| DescribeListElementMarker(node, description); |
| break; |
| default: |
| DescribeTypicalNode(node, description); |
| } |
| |
| return description; |
| } |
| |
| ScreenReaderMessageGenerator::UtteranceAndContext |
| ScreenReaderMessageGenerator::GenerateUtteranceByMessageId( |
| MessageIds message_id, zx::duration delay, const std::vector<std::string>& arg_names, |
| const std::vector<i18n::MessageFormatter::ArgValue>& arg_values) { |
| UtteranceAndContext utterance; |
| auto message = message_formatter_->FormatStringById(static_cast<uint64_t>(message_id), arg_names, |
| arg_values); |
| if (message != std::nullopt) { |
| utterance.utterance.set_message(std::move(*message)); |
| utterance.delay = delay; |
| } |
| return utterance; |
| } |
| |
| void ScreenReaderMessageGenerator::MaybeAddLabelDescriptor( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node); |
| |
| if (node->has_attributes() && node->attributes().has_label() && |
| !node->attributes().label().empty()) { |
| Utterance utterance; |
| utterance.set_message(node->attributes().label()); |
| description.emplace_back(UtteranceAndContext{.utterance = std::move(utterance)}); |
| } |
| } |
| |
| void ScreenReaderMessageGenerator::MaybeAddRoleDescriptor( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node); |
| |
| if (!node->has_role()) { |
| return; |
| } |
| |
| if (auto message_id = RoleToMessageId(node->role())) { |
| description.emplace_back(GenerateUtteranceByMessageId(*message_id)); |
| } |
| } |
| |
| void ScreenReaderMessageGenerator::MaybeAddGenericSelectedDescriptor( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>* description) { |
| FX_DCHECK(node); |
| FX_DCHECK(description); |
| |
| if (node->has_states() && node->states().has_selected() && node->states().selected()) { |
| description->emplace_back(GenerateUtteranceByMessageId(MessageIds::ELEMENT_SELECTED)); |
| } |
| } |
| |
| void ScreenReaderMessageGenerator::MaybeAddDoubleTapHint( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node); |
| |
| if (NodeIsClickable(node)) { |
| auto delay = description.empty() ? zx::msec(0) : kLongDelay; |
| |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::DOUBLE_TAP_HINT, delay)); |
| } |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeTypicalNode( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node); |
| |
| MaybeAddGenericSelectedDescriptor(node, &description); |
| MaybeAddLabelDescriptor(node, description); |
| MaybeAddRoleDescriptor(node, description); |
| MaybeAddDoubleTapHint(node, description); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeButton( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node->has_role() && node->role() == fuchsia::accessibility::semantics::Role::BUTTON); |
| |
| MaybeAddGenericSelectedDescriptor(node, &description); |
| MaybeAddLabelDescriptor(node, description); |
| |
| // Announce that the element is a button. |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_BUTTON)); |
| |
| // Announce the toggled state for the button, if set. |
| // |
| // Some UI elements have hybrid toggle/button semantics. |
| if (node->has_states() && node->states().has_toggled_state()) { |
| const auto message_id = |
| node->states().toggled_state() == fuchsia::accessibility::semantics::ToggledState::ON |
| ? MessageIds::ELEMENT_TOGGLED_ON |
| : MessageIds::ELEMENT_TOGGLED_OFF; |
| description.emplace_back(GenerateUtteranceByMessageId(message_id)); |
| } |
| |
| MaybeAddDoubleTapHint(node, description); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeRadioButton( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node->has_role() && |
| node->role() == fuchsia::accessibility::semantics::Role::RADIO_BUTTON); |
| |
| const auto message_id = |
| node->has_states() && node->states().has_selected() && node->states().selected() |
| ? MessageIds::RADIO_BUTTON_SELECTED |
| : MessageIds::RADIO_BUTTON_UNSELECTED; |
| const auto label = |
| node->has_attributes() && node->attributes().has_label() ? node->attributes().label() : ""; |
| |
| // Radio button is a special case: the label is part of the whole message that |
| // describes it. |
| description.emplace_back( |
| GenerateUtteranceByMessageId(message_id, zx::msec(0), {"name"}, {label})); |
| |
| MaybeAddDoubleTapHint(node, description); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeCheckBox( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node->has_role() && node->role() == fuchsia::accessibility::semantics::Role::CHECK_BOX); |
| |
| MaybeAddLabelDescriptor(node, description); |
| |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_CHECKBOX, kDefaultDelay)); |
| |
| if (node->has_states() && node->states().has_checked_state() && |
| node->states().checked_state() != fuchsia::accessibility::semantics::CheckedState::NONE) { |
| MessageIds message_id = MessageIds::ELEMENT_NOT_CHECKED; |
| switch (node->states().checked_state()) { |
| case fuchsia::accessibility::semantics::CheckedState::CHECKED: |
| message_id = MessageIds::ELEMENT_CHECKED; |
| break; |
| case fuchsia::accessibility::semantics::CheckedState::UNCHECKED: |
| message_id = MessageIds::ELEMENT_NOT_CHECKED; |
| break; |
| case fuchsia::accessibility::semantics::CheckedState::MIXED: |
| message_id = MessageIds::ELEMENT_PARTIALLY_CHECKED; |
| break; |
| case fuchsia::accessibility::semantics::CheckedState::NONE: |
| // When none is present, return without a description of the state. |
| return; |
| } |
| description.emplace_back(GenerateUtteranceByMessageId(message_id)); |
| } |
| |
| MaybeAddRoleDescriptor(node, description); |
| MaybeAddDoubleTapHint(node, description); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeListElementMarker( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| if (node->has_attributes() && node->attributes().has_label() && |
| !node->attributes().label().empty()) { |
| description.push_back(DescribeListElementMarkerLabel(node->attributes().label())); |
| } |
| |
| MaybeAddRoleDescriptor(node, description); |
| MaybeAddDoubleTapHint(node, description); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeToggleSwitch( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node->has_role() && |
| node->role() == fuchsia::accessibility::semantics::Role::TOGGLE_SWITCH); |
| |
| MaybeAddLabelDescriptor(node, description); |
| |
| const auto message_id = |
| node->has_states() && node->states().has_toggled_state() && |
| node->states().toggled_state() == fuchsia::accessibility::semantics::ToggledState::ON |
| ? MessageIds::ELEMENT_TOGGLED_ON |
| : MessageIds::ELEMENT_TOGGLED_OFF; |
| |
| description.emplace_back(GenerateUtteranceByMessageId(message_id)); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeSlider( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node); |
| FX_DCHECK(NodeIsSlider(node)); |
| |
| std::string message; |
| if (node->has_attributes() && node->attributes().has_label()) { |
| message += node->attributes().label(); |
| } |
| |
| const std::string slider_value = GetSliderValue(*node); |
| if (!slider_value.empty()) { |
| message = message + ", " + slider_value; |
| } |
| |
| Utterance utterance; |
| utterance.set_message(std::move(message)); |
| description.emplace_back(UtteranceAndContext{.utterance = std::move(utterance)}); |
| |
| MaybeAddRoleDescriptor(node, description); |
| MaybeAddDoubleTapHint(node, description); |
| } |
| |
| ScreenReaderMessageGenerator::UtteranceAndContext |
| ScreenReaderMessageGenerator::DescribeCharacterForSpelling(const std::string& character) { |
| const auto it = character_to_message_id_.find(character); |
| if (it != character_to_message_id_.end()) { |
| return GenerateUtteranceByMessageId(it->second); |
| } |
| |
| // TODO(https://fxbug.dev/42170854): Logic to detect uppercase letters may lead to bugs in non English |
| // locales. Checks if this character is uppercase. |
| if (character.size() == 1 && std::isupper(character[0])) { |
| return GenerateUtteranceByMessageId(MessageIds::CAPITALIZED_LETTER, zx::msec(0), {"letter"}, |
| {character}); |
| } |
| |
| UtteranceAndContext utterance; |
| utterance.utterance.set_message(character); |
| return utterance; |
| } |
| |
| ScreenReaderMessageGenerator::UtteranceAndContext |
| ScreenReaderMessageGenerator::DescribeListElementMarkerLabel(const std::string& label) { |
| const auto trimmed_label = std::string(fxl::TrimString(label, " \t")); |
| const auto it = character_to_message_id_.find(trimmed_label); |
| if (it != character_to_message_id_.end()) { |
| return GenerateUtteranceByMessageId(it->second); |
| } |
| |
| UtteranceAndContext utterance; |
| utterance.utterance.set_message(label); |
| return utterance; |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeTable( |
| const fuchsia::accessibility::semantics::Node& node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node.has_role() && node.role() == fuchsia::accessibility::semantics::Role::TABLE); |
| |
| if (node.has_attributes()) { |
| const auto& attributes = node.attributes(); |
| |
| // Add the table label to the description. |
| std::string label; |
| if (attributes.has_label() && !attributes.label().empty()) { |
| label = attributes.label(); |
| Utterance utterance; |
| utterance.set_message(label); |
| description.emplace_back(UtteranceAndContext{.utterance = std::move(utterance)}); |
| } |
| |
| // Add the table dimensions to the description. |
| if (attributes.has_table_attributes()) { |
| const auto& table_attributes = attributes.table_attributes(); |
| |
| // The table dimensions will only make sense if we have both the number of rows and the |
| // number of columns. |
| if (table_attributes.has_number_of_rows() && table_attributes.has_number_of_columns()) { |
| auto num_rows = std::to_string(table_attributes.number_of_rows()); |
| auto num_columns = std::to_string(table_attributes.number_of_columns()); |
| description.emplace_back( |
| GenerateUtteranceByMessageId(MessageIds::TABLE_DIMENSIONS, zx::msec(0), |
| {"num_rows", "num_columns"}, {num_rows, num_columns})); |
| } |
| } |
| } |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeTableCell( |
| const fuchsia::accessibility::semantics::Node* node, ScreenReaderMessageContext message_context, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node->has_role() && node->role() == fuchsia::accessibility::semantics::Role::CELL); |
| |
| if (node->has_attributes()) { |
| const auto& attributes = node->attributes(); |
| |
| // Add the cell label to the description. |
| std::string label; |
| if (attributes.has_label() && !attributes.label().empty()) { |
| if (message_context.changed_table_cell_context) { |
| // The message context will only have the row/column header fields |
| // populated if the user has navigated to a new row/column since the |
| // last cell was read. So, we can add them to the description unconditionally |
| // here if they are present. |
| if (!message_context.changed_table_cell_context->row_header.empty()) { |
| label += message_context.changed_table_cell_context->row_header + ", "; |
| } |
| |
| if (!message_context.changed_table_cell_context->column_header.empty()) { |
| label += message_context.changed_table_cell_context->column_header + ", "; |
| } |
| } |
| |
| label += attributes.label(); |
| |
| Utterance utterance; |
| utterance.set_message(label); |
| description.emplace_back(UtteranceAndContext{.utterance = std::move(utterance)}); |
| } |
| |
| // Add the cell row/column spans and row/column indices to the description. |
| if (attributes.has_table_cell_attributes()) { |
| const auto& table_cell_attributes = attributes.table_cell_attributes(); |
| |
| // We only want to speak the row span if it's > 1. |
| if (table_cell_attributes.has_row_span() && table_cell_attributes.row_span() > 1) { |
| auto row_span = std::to_string(table_cell_attributes.row_span()); |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROW_SPAN, zx::msec(0), |
| {"row_span"}, {row_span})); |
| } |
| |
| // We only want to speak the column span if it's > 1. |
| if (table_cell_attributes.has_column_span() && table_cell_attributes.column_span() > 1) { |
| auto column_span = std::to_string(table_cell_attributes.column_span()); |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::COLUMN_SPAN, zx::msec(0), |
| {"column_span"}, {column_span})); |
| } |
| |
| if (table_cell_attributes.has_row_index() && table_cell_attributes.has_column_index()) { |
| // We want to announce them as 1-indexed. |
| auto row_index = std::to_string(table_cell_attributes.row_index() + 1); |
| auto column_index = std::to_string(table_cell_attributes.column_index() + 1); |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::CELL_SUMMARY, zx::msec(0), |
| {"row_index", "column_index"}, |
| {row_index, column_index})); |
| } |
| } |
| } |
| |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_TABLE_CELL)); |
| |
| MaybeAddRoleDescriptor(node, description); |
| MaybeAddDoubleTapHint(node, description); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeRowOrColumnHeader( |
| const fuchsia::accessibility::semantics::Node* node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node->has_role() && |
| (node->role() == fuchsia::accessibility::semantics::Role::ROW_HEADER || |
| node->role() == fuchsia::accessibility::semantics::Role::COLUMN_HEADER)); |
| |
| if (node->has_attributes()) { |
| const auto& attributes = node->attributes(); |
| |
| // Add the label to the description. |
| std::string label; |
| if (attributes.has_label() && !attributes.label().empty()) { |
| Utterance utterance; |
| utterance.set_message(attributes.label()); |
| description.emplace_back(UtteranceAndContext{.utterance = std::move(utterance)}); |
| } |
| |
| if (attributes.has_table_cell_attributes()) { |
| const auto& table_cell_attributes = attributes.table_cell_attributes(); |
| |
| // Add the row/column index to the description. Note that only one of |
| // these should be set, depending on whether this header is a row or a |
| // column header. |
| if (table_cell_attributes.has_row_index()) { |
| // Row index should only be set for a row header. |
| FX_DCHECK(node->role() == fuchsia::accessibility::semantics::Role::ROW_HEADER); |
| // We want to announce it as 1-indexed. |
| auto row_index = std::to_string(table_cell_attributes.row_index() + 1); |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROW_SUMMARY, zx::msec(0), |
| {"row_index"}, {row_index})); |
| } |
| |
| if (table_cell_attributes.has_column_index()) { |
| // Column index should only be set for a column header. |
| FX_DCHECK(node->role() == fuchsia::accessibility::semantics::Role::COLUMN_HEADER); |
| // We want to announce it as 1-indexed. |
| auto column_index = std::to_string(table_cell_attributes.column_index() + 1); |
| description.emplace_back(GenerateUtteranceByMessageId( |
| MessageIds::COLUMN_SUMMARY, zx::msec(0), {"column_index"}, {column_index})); |
| } |
| |
| // Add the row/column span to the description. |
| if (table_cell_attributes.has_row_span() && table_cell_attributes.row_span() > 1) { |
| auto row_span = std::to_string(table_cell_attributes.row_span()); |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROW_SPAN, zx::msec(0), |
| {"row_span"}, {row_span})); |
| } |
| |
| if (table_cell_attributes.has_column_span() && table_cell_attributes.column_span() > 1) { |
| auto column_span = std::to_string(table_cell_attributes.column_span()); |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::COLUMN_SPAN, zx::msec(0), |
| {"column_span"}, {column_span})); |
| } |
| } |
| } |
| |
| if (node->role() == fuchsia::accessibility::semantics::Role::ROW_HEADER) { |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_TABLE_ROW_HEADER)); |
| } else { |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_TABLE_COLUMN_HEADER)); |
| } |
| |
| MaybeAddRoleDescriptor(node, description); |
| MaybeAddDoubleTapHint(node, description); |
| } |
| |
| void ScreenReaderMessageGenerator::DescribeEnteredList( |
| const fuchsia::accessibility::semantics::Node& node, |
| std::vector<UtteranceAndContext>& description) { |
| FX_DCHECK(node.has_role() && node.role() == fuchsia::accessibility::semantics::Role::LIST); |
| |
| if (node.has_attributes() && node.attributes().has_list_attributes() && |
| node.attributes().list_attributes().has_size()) { |
| description.emplace_back( |
| GenerateUtteranceByMessageId(MessageIds::ENTERED_LIST_DETAIL, zx::msec(0), {"num_items"}, |
| {node.attributes().list_attributes().size()})); |
| } else { |
| description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ENTERED_LIST)); |
| } |
| |
| // Add the list label to the description, if it's present. |
| if (node.has_attributes() && node.attributes().has_label() && |
| !node.attributes().label().empty()) { |
| Utterance utterance; |
| utterance.set_message(node.attributes().label()); |
| description.emplace_back(UtteranceAndContext{.utterance = std::move(utterance)}); |
| } |
| } |
| |
| } // namespace a11y |