| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/autofill/popup/popup_row_strategy.h" |
| |
| #include "base/feature_list.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/ui/autofill/autofill_popup_controller.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_base_view.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_cell_utils.h" |
| #include "components/autofill/core/browser/metrics/autofill_metrics.h" |
| #include "components/autofill/core/common/autofill_features.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/menu/menu_config.h" |
| #include "ui/views/controls/throbber.h" |
| #include "ui/views/layout/box_layout_view.h" |
| |
| namespace autofill { |
| |
| namespace { |
| |
| // The default icon size used in the suggestion drop down. |
| constexpr int kTrashCanLightIconSize = 12; |
| |
| // Max width for the username and masked password. |
| constexpr int kAutofillPopupUsernameMaxWidth = 272; |
| constexpr int kAutofillPopupPasswordMaxWidth = 108; |
| |
| constexpr int kAutocompleteDeleteIconHorizontalPadding = 8; |
| |
| // Popup items that use a leading icon instead of a trailing one. |
| constexpr PopupItemId kItemTypesUsingLeadingIcons[] = { |
| PopupItemId::kClearForm, |
| PopupItemId::kShowAccountCards, |
| PopupItemId::kAutofillOptions, |
| PopupItemId::kAllSavedPasswordsEntry, |
| PopupItemId::kPasswordAccountStorageEmpty, |
| PopupItemId::kPasswordAccountStorageOptIn, |
| PopupItemId::kPasswordAccountStorageReSignin, |
| PopupItemId::kPasswordAccountStorageOptInAndGenerate}; |
| |
| // ********************* AccessibilityDelegate implementations ***************** |
| |
| // ********************* ContentItemAccessibilityDelegate ********************* |
| class ContentItemAccessibilityDelegate |
| : public PopupCellView::AccessibilityDelegate { |
| public: |
| // Creates an a11y delegate for the `line_number`. `controller` must not be |
| // null. |
| ContentItemAccessibilityDelegate( |
| base::WeakPtr<AutofillPopupController> controller, |
| int line_number); |
| ~ContentItemAccessibilityDelegate() override = default; |
| |
| void GetAccessibleNodeData(bool is_selected, |
| ui::AXNodeData* node_data) const override; |
| |
| private: |
| // The string announced via VoiceOver. |
| std::u16string voice_over_string_; |
| // The number of suggestions in the popup and the (1-based) index of the |
| // suggestion this delegate belongs to. |
| int set_index_ = 0; |
| int set_size_ = 0; |
| }; |
| |
| ContentItemAccessibilityDelegate::ContentItemAccessibilityDelegate( |
| base::WeakPtr<AutofillPopupController> controller, |
| int line_number) { |
| DCHECK(controller); |
| |
| voice_over_string_ = popup_cell_utils::GetVoiceOverStringFromSuggestion( |
| controller->GetSuggestionAt(line_number)); |
| |
| set_size_ = 0; |
| set_index_ = line_number + 1; |
| for (int i = 0; i < controller->GetLineCount(); ++i) { |
| if (controller->GetSuggestionAt(i).popup_item_id == |
| PopupItemId::kSeparator) { |
| if (i < line_number) { |
| --set_index_; |
| } |
| } else { |
| ++set_size_; |
| } |
| } |
| } |
| |
| void ContentItemAccessibilityDelegate::GetAccessibleNodeData( |
| bool is_selected, |
| ui::AXNodeData* node_data) const { |
| DCHECK(node_data); |
| // Options are selectable. |
| node_data->role = ax::mojom::Role::kListBoxOption; |
| node_data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, is_selected); |
| node_data->SetNameChecked(voice_over_string_); |
| |
| node_data->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet, set_index_); |
| node_data->AddIntAttribute(ax::mojom::IntAttribute::kSetSize, set_size_); |
| } |
| |
| // ******************** DeleteButtonAccessibilityDelegate ********************* |
| class DeleteButtonAccessibilityDelegate |
| : public PopupCellView::AccessibilityDelegate { |
| public: |
| DeleteButtonAccessibilityDelegate( |
| base::WeakPtr<AutofillPopupController> controller, |
| int line_number); |
| ~DeleteButtonAccessibilityDelegate() override = default; |
| |
| void GetAccessibleNodeData(bool is_selected, |
| ui::AXNodeData* node_data) const override; |
| |
| private: |
| std::u16string voice_over_string_; |
| }; |
| |
| DeleteButtonAccessibilityDelegate::DeleteButtonAccessibilityDelegate( |
| base::WeakPtr<AutofillPopupController> controller, |
| int line_number) { |
| DCHECK(controller); |
| voice_over_string_ = l10n_util::GetStringFUTF16( |
| IDS_AUTOFILL_DELETE_AUTOCOMPLETE_SUGGESTION_A11Y_HINT, |
| popup_cell_utils::GetVoiceOverStringFromSuggestion( |
| controller->GetSuggestionAt(line_number))); |
| } |
| |
| void DeleteButtonAccessibilityDelegate::GetAccessibleNodeData( |
| bool is_selected, |
| ui::AXNodeData* node_data) const { |
| node_data->role = ax::mojom::Role::kMenuItem; |
| node_data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, is_selected); |
| node_data->SetNameChecked(voice_over_string_); |
| } |
| |
| } // namespace |
| |
| /**************************** PopupRowBaseStrategy ****************************/ |
| |
| PopupRowBaseStrategy::PopupRowBaseStrategy( |
| base::WeakPtr<AutofillPopupController> controller, |
| int line_number) |
| : controller_(controller), line_number_(line_number) { |
| DCHECK(controller_); |
| } |
| |
| PopupRowBaseStrategy::~PopupRowBaseStrategy() = default; |
| |
| int PopupRowBaseStrategy::GetLineNumber() const { |
| return line_number_; |
| } |
| |
| /************************** PopupSuggestionStrategy ***************************/ |
| |
| PopupSuggestionStrategy::PopupSuggestionStrategy( |
| base::WeakPtr<AutofillPopupController> controller, |
| int line_number) |
| : PopupRowBaseStrategy(std::move(controller), line_number), |
| popup_type_(GetController()->GetPopupType()) {} |
| |
| PopupSuggestionStrategy::~PopupSuggestionStrategy() = default; |
| |
| std::unique_ptr<PopupCellView> PopupSuggestionStrategy::CreateContent() { |
| if (!GetController()) { |
| return nullptr; |
| } |
| const Suggestion& kSuggestion = |
| GetController()->GetSuggestionAt(GetLineNumber()); |
| std::unique_ptr<PopupCellView> view = |
| views::Builder<PopupCellView>() |
| .SetAccessibilityDelegate( |
| std::make_unique<ContentItemAccessibilityDelegate>( |
| GetController(), GetLineNumber())) |
| .Build(); |
| |
| // Add the actual views. |
| std::unique_ptr<views::Label> main_text_label = |
| popup_cell_utils::CreateMainTextLabel( |
| kSuggestion.main_text, views::style::TextStyle::STYLE_PRIMARY); |
| popup_cell_utils::FormatLabel(*main_text_label, kSuggestion.main_text, |
| GetController()); |
| popup_cell_utils::AddSuggestionContentToView( |
| kSuggestion, std::move(main_text_label), |
| popup_cell_utils::CreateMinorTextLabel(kSuggestion.minor_text), |
| /*description_label=*/nullptr, |
| popup_cell_utils::CreateAndTrackSubtextViews(*view, GetController(), |
| GetLineNumber()), |
| *view); |
| |
| // Prepare the callbacks to the controller. |
| popup_cell_utils::AddCallbacksToContentView(GetController(), GetLineNumber(), |
| *view); |
| |
| return view; |
| } |
| |
| std::unique_ptr<PopupCellView> PopupSuggestionStrategy::CreateControl() { |
| if (!GetController()) { |
| return nullptr; |
| } |
| |
| // If the feature is enabled, autocomplete entries have a delete button. |
| if (GetController()->GetSuggestionAt(GetLineNumber()).popup_item_id == |
| PopupItemId::kAutocompleteEntry && |
| base::FeatureList::IsEnabled( |
| features::kAutofillShowAutocompleteDeleteButton)) { |
| std::unique_ptr<PopupCellView> view = |
| views::Builder<PopupCellView>() |
| .SetAccessibilityDelegate( |
| std::make_unique<DeleteButtonAccessibilityDelegate>( |
| GetController(), GetLineNumber())) |
| .Build(); |
| |
| view->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kHorizontal, |
| gfx::Insets::VH(0, kAutocompleteDeleteIconHorizontalPadding))); |
| views::ImageView* delete_icon = |
| view->AddChildView(popup_cell_utils::ImageViewFromVectorIcon( |
| kTrashCanLightIcon, kTrashCanLightIconSize)); |
| // The tooltip is set for both the cell and the image to ensure that it is |
| // also shown over the padding area. |
| delete_icon->SetTooltipText(l10n_util::GetStringUTF16( |
| IDS_AUTOFILL_DELETE_AUTOCOMPLETE_SUGGESTION_TOOLTIP)); |
| view->SetTooltipText(l10n_util::GetStringUTF16( |
| IDS_AUTOFILL_DELETE_AUTOCOMPLETE_SUGGESTION_TOOLTIP)); |
| view->SetOnAcceptedCallback(base::BindRepeating( |
| [](base::WeakPtr<AutofillPopupController> controller, int line_number) { |
| if (controller && controller->RemoveSuggestion(line_number)) { |
| AutofillMetrics::OnAutocompleteSuggestionDeleted( |
| AutofillMetrics::AutocompleteSingleEntryRemovalMethod:: |
| kDeleteButtonClicked); |
| } |
| }, |
| GetController(), GetLineNumber())); |
| |
| return view; |
| } |
| |
| return nullptr; |
| } |
| |
| /************************ PopupPasswordSuggestionStrategy *******************/ |
| |
| PopupPasswordSuggestionStrategy::PopupPasswordSuggestionStrategy( |
| base::WeakPtr<AutofillPopupController> controller, |
| int line_number) |
| : PopupRowBaseStrategy(std::move(controller), line_number) {} |
| |
| PopupPasswordSuggestionStrategy::~PopupPasswordSuggestionStrategy() = default; |
| |
| std::unique_ptr<PopupCellView> |
| PopupPasswordSuggestionStrategy::CreateContent() { |
| if (!GetController()) { |
| return nullptr; |
| } |
| |
| const Suggestion& kSuggestion = |
| GetController()->GetSuggestionAt(GetLineNumber()); |
| std::unique_ptr<PopupCellView> view = |
| views::Builder<PopupCellView>() |
| .SetAccessibilityDelegate( |
| std::make_unique<ContentItemAccessibilityDelegate>( |
| GetController(), GetLineNumber())) |
| .Build(); |
| |
| // Add the actual views. |
| std::unique_ptr<views::Label> main_text_label = |
| popup_cell_utils::CreateMainTextLabel( |
| kSuggestion.main_text, views::style::TextStyle::STYLE_PRIMARY); |
| main_text_label->SetMaximumWidthSingleLine(kAutofillPopupUsernameMaxWidth); |
| |
| popup_cell_utils::AddSuggestionContentToView( |
| kSuggestion, std::move(main_text_label), |
| popup_cell_utils::CreateMinorTextLabel(kSuggestion.minor_text), |
| CreateDescriptionLabel(), CreateAndTrackSubtextViews(*view), *view); |
| |
| // Prepare the callbacks to the controller. |
| popup_cell_utils::AddCallbacksToContentView(GetController(), GetLineNumber(), |
| *view); |
| |
| return view; |
| } |
| |
| std::unique_ptr<views::Label> |
| PopupPasswordSuggestionStrategy::CreateDescriptionLabel() const { |
| const Suggestion& kSuggestion = |
| GetController()->GetSuggestionAt(GetLineNumber()); |
| if (kSuggestion.labels.empty()) { |
| return nullptr; |
| } |
| |
| DCHECK_EQ(kSuggestion.labels.size(), 1u); |
| DCHECK_EQ(kSuggestion.labels[0].size(), 1u); |
| |
| auto label = std::make_unique<views::Label>( |
| kSuggestion.labels[0][0].value, views::style::CONTEXT_DIALOG_BODY_TEXT, |
| views::style::STYLE_SECONDARY); |
| label->SetElideBehavior(gfx::ELIDE_HEAD); |
| label->SetMaximumWidthSingleLine(kAutofillPopupUsernameMaxWidth); |
| return label; |
| } |
| |
| std::vector<std::unique_ptr<views::View>> |
| PopupPasswordSuggestionStrategy::CreateAndTrackSubtextViews( |
| PopupCellView& content_view) const { |
| std::unique_ptr<views::Label> label = std::make_unique<views::Label>( |
| GetController()->GetSuggestionAt(GetLineNumber()).additional_label, |
| views::style::CONTEXT_DIALOG_BODY_TEXT, views::style::STYLE_SECONDARY); |
| label->SetElideBehavior(gfx::TRUNCATE); |
| label->SetMaximumWidthSingleLine(kAutofillPopupPasswordMaxWidth); |
| content_view.TrackLabel(label.get()); |
| std::vector<std::unique_ptr<views::View>> result; |
| result.push_back(std::move(label)); |
| return result; |
| } |
| |
| std::unique_ptr<PopupCellView> |
| PopupPasswordSuggestionStrategy::CreateControl() { |
| return nullptr; |
| } |
| |
| /************************** PopupFooterStrategy ******************************/ |
| |
| PopupFooterStrategy::PopupFooterStrategy( |
| base::WeakPtr<AutofillPopupController> controller, |
| int line_number) |
| : PopupRowBaseStrategy(std::move(controller), line_number) {} |
| |
| PopupFooterStrategy::~PopupFooterStrategy() = default; |
| |
| std::unique_ptr<PopupCellView> PopupFooterStrategy::CreateContent() { |
| if (!GetController()) { |
| return nullptr; |
| } |
| |
| const Suggestion& kSuggestion = |
| GetController()->GetSuggestionAt(GetLineNumber()); |
| std::unique_ptr<PopupCellView> view = |
| views::Builder<PopupCellView>() |
| .SetAccessibilityDelegate( |
| std::make_unique<ContentItemAccessibilityDelegate>( |
| GetController(), GetLineNumber())) |
| .Build(); |
| |
| views::BoxLayout* layout_manager = |
| view->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kHorizontal, |
| popup_cell_utils::GetMarginsForContentCell( |
| /*has_control_element=*/false))); |
| |
| layout_manager->set_cross_axis_alignment( |
| views::BoxLayout::CrossAxisAlignment::kCenter); |
| |
| std::unique_ptr<views::ImageView> icon = |
| popup_cell_utils::GetIconImageView(kSuggestion); |
| |
| const bool kUseLeadingIcon = |
| base::Contains(kItemTypesUsingLeadingIcons, kSuggestion.popup_item_id); |
| |
| if (kSuggestion.is_loading) { |
| view->AddChildView(std::make_unique<views::Throbber>())->Start(); |
| popup_cell_utils::AddSpacerWithSize(*view, *layout_manager, |
| PopupBaseView::GetHorizontalPadding(), |
| /*resize=*/false); |
| } else if (icon && kUseLeadingIcon) { |
| view->AddChildView(std::move(icon)); |
| popup_cell_utils::AddSpacerWithSize(*view, *layout_manager, |
| PopupBaseView::GetHorizontalPadding(), |
| /*resize=*/false); |
| } |
| |
| layout_manager->set_minimum_cross_axis_size( |
| views::MenuConfig::instance().touchable_menu_height); |
| |
| std::unique_ptr<views::Label> main_text_label = |
| popup_cell_utils::CreateMainTextLabel(kSuggestion.main_text, |
| views::style::STYLE_SECONDARY); |
| main_text_label->SetEnabled(!kSuggestion.is_loading); |
| view->TrackLabel(view->AddChildView(std::move(main_text_label))); |
| |
| popup_cell_utils::AddSpacerWithSize(*view, *layout_manager, 0, |
| /*resize=*/true); |
| |
| if (icon && !kUseLeadingIcon) { |
| popup_cell_utils::AddSpacerWithSize(*view, *layout_manager, |
| PopupBaseView::GetHorizontalPadding(), |
| /*resize=*/false); |
| view->AddChildView(std::move(icon)); |
| } |
| |
| std::unique_ptr<views::ImageView> trailing_icon = |
| popup_cell_utils::GetTrailingIconImageView(kSuggestion); |
| if (trailing_icon) { |
| popup_cell_utils::AddSpacerWithSize(*view, *layout_manager, |
| PopupBaseView::GetHorizontalPadding(), |
| /*resize=*/true); |
| view->AddChildView(std::move(trailing_icon)); |
| } |
| |
| // Force a refresh to ensure all the labels'styles are correct. |
| view->RefreshStyle(); |
| |
| popup_cell_utils::AddCallbacksToContentView(GetController(), GetLineNumber(), |
| *view); |
| |
| return view; |
| } |
| |
| std::unique_ptr<PopupCellView> PopupFooterStrategy::CreateControl() { |
| return nullptr; |
| } |
| |
| } // namespace autofill |