| // Copyright 2017 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_view_views.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <type_traits> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/notreached.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/platform_util.h" |
| #include "chrome/browser/ui/autofill/autofill_popup_controller.h" |
| #include "chrome/browser/ui/autofill/autofill_suggestion_controller_utils.h" |
| #include "chrome/browser/ui/browser_element_identifiers.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/passwords/ui_utils.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_base_view.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_row_factory_utils.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_row_view.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_search_bar_view.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_separator_view.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_title_view.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_view_utils.h" |
| #include "chrome/browser/ui/views/autofill/popup/popup_warning_view.h" |
| #include "chrome/browser/ui/views/chrome_layout_provider.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "components/autofill/core/browser/autofill_experiments.h" |
| #include "components/autofill/core/browser/data_model/credit_card.h" |
| #include "components/autofill/core/browser/metrics/autofill_metrics.h" |
| #include "components/autofill/core/browser/ui/autofill_resource_utils.h" |
| #include "components/autofill/core/browser/ui/suggestion.h" |
| #include "components/autofill/core/browser/ui/suggestion_hiding_reason.h" |
| #include "components/autofill/core/browser/ui/suggestion_type.h" |
| #include "components/autofill/core/common/aliases.h" |
| #include "components/autofill/core/common/autofill_features.h" |
| #include "components/autofill/core/common/autofill_payments_features.h" |
| #include "components/feature_engagement/public/feature_constants.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/user_education/common/feature_promo_controller.h" |
| #include "content/public/common/input/native_web_keyboard_event.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "third_party/blink/public/common/input/web_input_event.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/color/color_id.h" |
| #include "ui/events/blink/web_input_event.h" |
| #include "ui/events/event.h" |
| #include "ui/events/keycodes/keyboard_codes.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/gfx/geometry/rounded_corners_f.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/background.h" |
| #include "ui/views/border.h" |
| #include "ui/views/bubble/bubble_border.h" |
| #include "ui/views/bubble/bubble_border_arrow_utils.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/box_layout_view.h" |
| #include "ui/views/layout/flex_layout.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_class_properties.h" |
| #include "ui/views/widget/widget.h" |
| |
| using views::BubbleBorder; |
| |
| namespace autofill { |
| |
| namespace { |
| |
| // By spec, dropdowns should always have a width which is a multiple of 12. |
| constexpr int kAutofillPopupWidthMultiple = 12; |
| |
| // The minimum width should exceed the maximum size of a cursor, which is 128 |
| // (see crbug.com/1434330). |
| constexpr int kAutofillPopupMinWidth = kAutofillPopupWidthMultiple * 13; |
| static_assert(kAutofillPopupMinWidth > 128); |
| // TODO(crbug.com/41382463): move handling the max width to the base class. |
| constexpr int kAutofillPopupMaxWidth = kAutofillPopupWidthMultiple * 38; |
| |
| // Preferred position relative to the control sides of the sub-popup. |
| constexpr std::array<views::BubbleArrowSide, 2> kDefaultSubPopupSides = { |
| views::BubbleArrowSide::kLeft, views::BubbleArrowSide::kRight}; |
| constexpr std::array<views::BubbleArrowSide, 2> kDefaultSubPopupSidesRTL = { |
| views::BubbleArrowSide::kRight, views::BubbleArrowSide::kLeft}; |
| |
| int GetContentsVerticalPadding() { |
| return ChromeLayoutProvider::Get()->GetDistanceMetric( |
| DISTANCE_CONTENT_LIST_VERTICAL_SINGLE); |
| } |
| |
| bool CanShowRootPopup(AutofillSuggestionController& controller) { |
| #if BUILDFLAG(IS_MAC) |
| // It's possible for the container_view to not be in a window. In that case, |
| // cancel the popup since we can't fully set it up. |
| if (!platform_util::GetTopLevel(controller.container_view())) { |
| return false; |
| } |
| #else |
| // If the top level widget can't be found, cancel the popup since we can't |
| // fully set it up. On Mac Cocoa browser, |observing_widget| is null |
| // because the parent is not a views::Widget. |
| if (!views::Widget::GetTopLevelWidgetForNativeView( |
| controller.container_view())) { |
| return false; |
| } |
| #endif |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| // Creates a new popup view instance. The Widget parent is taken either from |
| // the top level widget for the root popup or from the parent for sub-popups. |
| // Setting Widget's parent enables its internal child-lifetime management, |
| // see `Widget::InitParams::parent` doc comment for details. |
| PopupViewViews::PopupViewViews( |
| base::WeakPtr<AutofillPopupController> controller, |
| base::WeakPtr<ExpandablePopupParentView> parent, |
| views::Widget* parent_widget) |
| : PopupBaseView(controller, |
| parent_widget, |
| views::Widget::InitParams::Activatable::kDefault, |
| base::i18n::IsRTL() ? kDefaultSubPopupSidesRTL |
| : kDefaultSubPopupSides, |
| /*show_arrow_pointer=*/false), |
| controller_(controller), |
| parent_(parent) { |
| InitViews({}); |
| } |
| |
| PopupViewViews::PopupViewViews( |
| base::WeakPtr<AutofillPopupController> controller, |
| PopupViewSearchBarConfig search_bar_config) |
| : PopupBaseView(controller, |
| views::Widget::GetTopLevelWidgetForNativeView( |
| controller->container_view()), |
| search_bar_config.enabled |
| ? views::Widget::InitParams::Activatable::kYes |
| : views::Widget::InitParams::Activatable::kDefault), |
| controller_(controller) { |
| InitViews(search_bar_config); |
| } |
| |
| PopupViewViews::~PopupViewViews() = default; |
| |
| void PopupViewViews::GetAccessibleNodeData(ui::AXNodeData* node_data) { |
| node_data->role = ax::mojom::Role::kListBox; |
| // If controller_ is valid, then the view is expanded. |
| if (controller_) { |
| node_data->AddState(ax::mojom::State::kExpanded); |
| } else { |
| node_data->AddState(ax::mojom::State::kCollapsed); |
| node_data->AddState(ax::mojom::State::kInvisible); |
| } |
| node_data->SetNameChecked( |
| l10n_util::GetStringUTF16(IDS_AUTOFILL_POPUP_ACCESSIBLE_NODE_DATA)); |
| } |
| |
| void PopupViewViews::OnMouseEntered(const ui::MouseEvent& event) { |
| OnMouseEnteredInChildren(); |
| } |
| |
| void PopupViewViews::OnMouseExited(const ui::MouseEvent& event) { |
| OnMouseExitedInChildren(); |
| } |
| |
| bool PopupViewViews::Show( |
| AutoselectFirstSuggestion autoselect_first_suggestion) { |
| base::AutoReset show_in_progress_reset(&show_in_progress_, !!search_bar_); |
| |
| NotifyAccessibilityEvent(ax::mojom::Event::kExpandedChanged, true); |
| if (!DoShow()) { |
| return false; |
| } |
| |
| has_keyboard_focus_ = !parent_; |
| |
| if (autoselect_first_suggestion) { |
| SetSelectedCell(CellIndex{0u, PopupRowView::CellType::kContent}, |
| PopupCellSelectionSource::kNonUserInput); |
| } |
| |
| // Check for the special "warning bubble" mode: single warning suggestion |
| // which content should be just announced to the user. Triggering |
| // Event::kAlert on such a row makes screen readers read its content out. |
| // TODO(crbug.com/40281426): Consider supporting "warning mode" explicitly. |
| if (rows_.size() == 1 && |
| absl::holds_alternative<PopupWarningView*>(rows_[0])) { |
| absl::get<PopupWarningView*>(rows_[0])->NotifyAccessibilityEvent( |
| ax::mojom::Event::kAlert, true); |
| } |
| // Compose popups are announced separately. |
| if (controller_->GetMainFillingProduct() == FillingProduct::kCompose) { |
| AxAnnounce( |
| l10n_util::GetStringUTF16(IDS_COMPOSE_SUGGESTION_AX_MESSAGE_ON_SHOW)); |
| } |
| |
| if (search_bar_) { |
| search_bar_->Focus(); |
| } |
| |
| return !CanActivate() || (GetWidget() && GetWidget()->IsActive()); |
| } |
| |
| void PopupViewViews::Hide() { |
| NotifyAccessibilityEvent(ax::mojom::Event::kExpandedChanged, true); |
| |
| open_sub_popup_timer_.Stop(); |
| no_selection_sub_popup_close_timer_.Stop(); |
| |
| // The controller is no longer valid after it hides us. |
| controller_ = nullptr; |
| DoHide(); |
| } |
| |
| std::optional<PopupViewViews::CellIndex> PopupViewViews::GetSelectedCell() |
| const { |
| // If the suggestions were updated, the cell index may no longer be |
| // up-to-date, but it cannot simply be reset, because we would lose the |
| // current selection. Therefore some validity checks need to be performed |
| // here. |
| if (!row_with_selected_cell_ || |
| !HasPopupRowViewAt(*row_with_selected_cell_)) { |
| return std::nullopt; |
| } |
| |
| if (std::optional<PopupRowView::CellType> cell_type = |
| GetPopupRowViewAt(*row_with_selected_cell_).GetSelectedCell()) { |
| return CellIndex{*row_with_selected_cell_, *cell_type}; |
| } |
| return std::nullopt; |
| } |
| |
| void PopupViewViews::SetSelectedCell(std::optional<CellIndex> cell_index, |
| PopupCellSelectionSource source) { |
| SetSelectedCell(cell_index, source, AutoselectFirstSuggestion(false)); |
| } |
| |
| bool PopupViewViews::HandleKeyPressEvent( |
| const content::NativeWebKeyboardEvent& event) { |
| // If a subpopup has not received focus yet but a horizontal key press event |
| // happens, this means the user wants to navigate from a selected cell in |
| // the parent to the currently open subpopup. In this case, we select |
| // the first subpopup cell. |
| if (!has_keyboard_focus_) { |
| bool capture_keyboard_focus = |
| (event.windows_key_code == ui::VKEY_RIGHT && !base::i18n::IsRTL()) || |
| (event.windows_key_code == ui::VKEY_LEFT && base::i18n::IsRTL()); |
| if (capture_keyboard_focus) { |
| SetSelectedCell(CellIndex{0u, PopupRowView::CellType::kContent}, |
| PopupCellSelectionSource::kKeyboard); |
| return true; |
| } |
| return false; |
| } |
| |
| // If the row can handle the event itself (e.g. switching between cells in the |
| // same row), we let it. |
| if (std::optional<CellIndex> selected_cell = GetSelectedCell()) { |
| if (GetPopupRowViewAt(selected_cell->first).HandleKeyPressEvent(event)) { |
| return true; |
| } |
| } |
| |
| if (controller_->GetMainFillingProduct() == FillingProduct::kCompose) { |
| return HandleKeyPressEventForCompose(event); |
| } |
| |
| const bool kHasShiftModifier = |
| (event.GetModifiers() & blink::WebInputEvent::kShiftKey); |
| const bool kHasNonShiftModifier = |
| (event.GetModifiers() & blink::WebInputEvent::kKeyModifiers & |
| ~blink::WebInputEvent::kShiftKey); |
| |
| switch (event.windows_key_code) { |
| case ui::VKEY_UP: |
| SelectPreviousRow(); |
| return true; |
| case ui::VKEY_DOWN: |
| SelectNextRow(); |
| return true; |
| case ui::VKEY_LEFT: |
| // `base::i18n::IsRTL` is used here instead of the controller's method |
| // because the controller's `IsRTL` depends on the language of the focused |
| // field and not the overall UI language. However, the layout of the popup |
| // is determined by the overall UI language. |
| if (base::i18n::IsRTL()) { |
| return SelectNextHorizontalCell(); |
| } else { |
| if (SelectParentPopupContentCell()) { |
| return true; |
| } |
| return SelectPreviousHorizontalCell(); |
| } |
| case ui::VKEY_RIGHT: |
| if (base::i18n::IsRTL()) { |
| if (SelectParentPopupContentCell()) { |
| return true; |
| } |
| return SelectPreviousHorizontalCell(); |
| } else { |
| return SelectNextHorizontalCell(); |
| } |
| case ui::VKEY_PRIOR: // Page up. |
| // Set no line and then select the next line in case the first line is not |
| // selectable. |
| SetSelectedCell(std::nullopt, PopupCellSelectionSource::kKeyboard); |
| SelectNextRow(); |
| return true; |
| case ui::VKEY_NEXT: // Page down. |
| SetSelectedCell(std::nullopt, PopupCellSelectionSource::kKeyboard); |
| SelectPreviousRow(); |
| return true; |
| case ui::VKEY_DELETE: |
| return kHasShiftModifier && RemoveSelectedCell(); |
| case ui::VKEY_ESCAPE: |
| if (SelectParentPopupContentCell()) { |
| return true; |
| } |
| // If this is the root popup view and there was no sub-popup open (find |
| // the check for it above) just close itself. |
| if (!parent_) { |
| controller_->Hide(SuggestionHidingReason::kUserAborted); |
| return true; |
| } |
| return false; |
| case ui::VKEY_TAB: |
| // We want TAB or Shift+TAB press to cause the selected line to be |
| // accepted, but still return false so the tab key press propagates and |
| // change the cursor location. |
| // We do not want to handle Mod+TAB for other modifiers because this may |
| // have other purposes (e.g., change the tab). |
| if (!kHasNonShiftModifier) { |
| AcceptSelectedContentOrCreditCardCell(); |
| } |
| return false; |
| default: |
| return false; |
| } |
| } |
| |
| bool PopupViewViews::HandleKeyPressEventForCompose( |
| const content::NativeWebKeyboardEvent& event) { |
| CHECK_EQ(controller_->GetMainFillingProduct(), FillingProduct::kCompose); |
| const bool kHasShiftModifier = |
| (event.GetModifiers() & blink::WebInputEvent::kShiftKey); |
| switch (event.windows_key_code) { |
| case ui::VKEY_ESCAPE: |
| controller_->Hide(SuggestionHidingReason::kUserAborted); |
| return true; |
| case ui::VKEY_UP: |
| if (GetSelectedCell()) { |
| SelectPreviousRow(); |
| return true; |
| } |
| return false; |
| case ui::VKEY_DOWN: |
| if (GetSelectedCell()) { |
| SelectNextRow(); |
| return true; |
| } |
| return false; |
| case ui::VKEY_LEFT: |
| // `base::i18n::IsRTL` is used here instead of the controller's method |
| // because the controller's `IsRTL` depends on the language of the focused |
| // field and not the overall UI language. However, the layout of the popup |
| // is determined by the overall UI language. |
| if (base::i18n::IsRTL()) { |
| return SelectNextHorizontalCell(); |
| } else { |
| if (SelectParentPopupContentCell()) { |
| return true; |
| } |
| return SelectPreviousHorizontalCell(); |
| } |
| case ui::VKEY_RIGHT: |
| if (base::i18n::IsRTL()) { |
| if (SelectParentPopupContentCell()) { |
| return true; |
| } |
| return SelectPreviousHorizontalCell(); |
| } else { |
| return SelectNextHorizontalCell(); |
| } |
| case ui::VKEY_TAB: { |
| const bool is_root_popup = !parent_; |
| // TAB should only be handled by the root popup. The subpopup only deals |
| // with selection (ENTER) and arrow navigation. |
| if (!is_root_popup) { |
| return false; |
| } |
| std::optional<CellIndex> selected_cell = GetSelectedCell(); |
| // The `!row_with_open_sub_popup_` check is to make sure that we only |
| // select the content cell if there is no subpopup open. This is because |
| // if one presses TAB from the subpopup, we also want to close the root |
| // popup (and navigate to the next HTML element). |
| const bool tab_pressed_popup_unselected = |
| !selected_cell && !kHasShiftModifier && !row_with_open_sub_popup_; |
| if (tab_pressed_popup_unselected) { |
| // If there is no selected cell in the compose popup, TAB should select |
| // the single compose nudge entry. |
| SetSelectedCell(CellIndex(0, PopupRowView::CellType::kContent), |
| PopupCellSelectionSource::kKeyboard); |
| return true; |
| } |
| |
| const bool tab_pressed_popup_selected = |
| selected_cell && !kHasShiftModifier; |
| if (tab_pressed_popup_selected) { |
| // TAB should close the popup and focus the next HTML element if the |
| // Compose entry is selected. |
| controller_->Hide(SuggestionHidingReason::kUserAborted); |
| return false; |
| } |
| |
| const bool shift_tab_pressed_popup_unselected_no_subpopup = |
| !selected_cell && kHasShiftModifier && !row_with_open_sub_popup_; |
| if (shift_tab_pressed_popup_unselected_no_subpopup) { |
| // If the Compose suggestion is not selected, Shift+TAB should not be |
| // handled. |
| return false; |
| } |
| |
| const bool shift_tab_pressed_has_subpopup = |
| kHasShiftModifier && row_with_open_sub_popup_; |
| if (shift_tab_pressed_has_subpopup) { |
| // In this case, focus on the root/parent popup content area. This |
| // closes the sub-popup. |
| SetSelectedCell(CellIndex(0, PopupRowView::CellType::kContent), |
| PopupCellSelectionSource::kKeyboard); |
| return true; |
| } |
| |
| const bool shift_tab_pressed_root_popup_selected = |
| selected_cell && kHasShiftModifier && is_root_popup; |
| if (shift_tab_pressed_root_popup_selected) { |
| // Shift+TAB should remove the selection when the root popup is |
| // selected, but keep the popup open. |
| SetSelectedCell(std::nullopt, PopupCellSelectionSource::kKeyboard); |
| return true; |
| } |
| return false; |
| } |
| default: |
| return false; |
| } |
| } |
| |
| void PopupViewViews::SelectPreviousRow() { |
| DCHECK(!rows_.empty()); |
| std::optional<CellIndex> old_index = GetSelectedCell(); |
| |
| // Temporarily use an int to avoid underflows. |
| int new_row = old_index ? static_cast<int>(old_index->first) - 1 : -1; |
| while (new_row >= 0 && !HasPopupRowViewAt(new_row)) { |
| --new_row; |
| } |
| |
| if (new_row < 0) { |
| new_row = static_cast<int>(rows_.size()) - 1; |
| } |
| |
| // `kControl` is used to show a sub-popup with child suggestions. It can only |
| // be selected on a new row if the corresponding suggestion has children. |
| const PopupRowView::CellType kNewCellType = |
| (old_index && old_index->second == PopupRowView::CellType::kControl && |
| GetPopupRowViewAt(new_row).GetExpandChildSuggestionsView()) |
| ? PopupRowView::CellType::kControl |
| : PopupRowView::CellType::kContent; |
| |
| SetSelectedCell(CellIndex{new_row, kNewCellType}, |
| PopupCellSelectionSource::kKeyboard); |
| } |
| |
| void PopupViewViews::SelectNextRow() { |
| DCHECK(!rows_.empty()); |
| std::optional<CellIndex> old_index = GetSelectedCell(); |
| |
| size_t new_row = old_index ? old_index->first + 1u : 0u; |
| while (new_row < rows_.size() && !HasPopupRowViewAt(new_row)) { |
| ++new_row; |
| } |
| |
| if (new_row >= rows_.size()) { |
| new_row = 0u; |
| } |
| |
| // `kControl` is used to show a sub-popup with child suggestions. It can only |
| // be selected on a new row if the corresponding suggestion has children. |
| const PopupRowView::CellType kNewCellType = |
| (old_index && old_index->second == PopupRowView::CellType::kControl && |
| GetPopupRowViewAt(new_row).GetExpandChildSuggestionsView()) |
| ? PopupRowView::CellType::kControl |
| : PopupRowView::CellType::kContent; |
| |
| SetSelectedCell(CellIndex{new_row, kNewCellType}, |
| PopupCellSelectionSource::kKeyboard); |
| } |
| |
| bool PopupViewViews::SelectNextHorizontalCell() { |
| std::optional<CellIndex> selected_cell = GetSelectedCell(); |
| if (selected_cell && HasPopupRowViewAt(selected_cell->first)) { |
| PopupRowView& row = GetPopupRowViewAt(selected_cell->first); |
| if (selected_cell->second == PopupRowView::CellType::kContent && |
| row.GetExpandChildSuggestionsView()) { |
| SetSelectedCell( |
| CellIndex{selected_cell->first, PopupRowView::CellType::kControl}, |
| PopupCellSelectionSource::kKeyboard, AutoselectFirstSuggestion(true)); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool PopupViewViews::SelectPreviousHorizontalCell() { |
| std::optional<CellIndex> selected_cell = GetSelectedCell(); |
| if (selected_cell && |
| selected_cell->second == PopupRowView::CellType::kControl && |
| HasPopupRowViewAt(selected_cell->first)) { |
| SetSelectedCell( |
| CellIndex{selected_cell->first, PopupRowView::CellType::kContent}, |
| PopupCellSelectionSource::kKeyboard); |
| return true; |
| } |
| return false; |
| } |
| |
| bool PopupViewViews::AcceptSelectedContentOrCreditCardCell() { |
| std::optional<CellIndex> index = GetSelectedCell(); |
| if (!controller_ || !index) { |
| return false; |
| } |
| |
| if (index->second != PopupRowView::CellType::kContent) { |
| return false; |
| } |
| |
| const SuggestionType type = controller_->GetSuggestionAt(index->first).type; |
| if (!base::Contains(kItemsTriggeringFieldFilling, type) && |
| type != SuggestionType::kScanCreditCard) { |
| return false; |
| } |
| |
| controller_->AcceptSuggestion(index->first); |
| return true; |
| } |
| |
| bool PopupViewViews::RemoveSelectedCell() { |
| std::optional<CellIndex> index = GetSelectedCell(); |
| |
| // Only content cells can be removed. |
| if (!index || index->second != PopupRowView::CellType::kContent || |
| !controller_) { |
| return false; |
| } |
| |
| if (!controller_->RemoveSuggestion(index->first, |
| AutofillMetrics::SingleEntryRemovalMethod:: |
| kKeyboardShiftDeletePressed)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void PopupViewViews::OnSuggestionsChanged() { |
| // New suggestions invalidate this scheduling (if it's running), cancel it. |
| open_sub_popup_timer_.Stop(); |
| SetRowWithOpenSubPopup(std::nullopt); |
| |
| CreateSuggestionViews(); |
| DoUpdateBoundsAndRedrawPopup(); |
| } |
| |
| bool PopupViewViews::OverlapsWithPictureInPictureWindow() const { |
| return BoundsOverlapWithPictureInPictureWindow(GetBoundsInScreen()); |
| } |
| |
| std::optional<int32_t> PopupViewViews::GetAxUniqueId() { |
| return std::optional<int32_t>( |
| PopupBaseView::GetViewAccessibility().GetUniqueId()); |
| } |
| |
| void PopupViewViews::AxAnnounce(const std::u16string& text) { |
| Browser* browser = chrome::FindLastActive(); |
| if (!browser) { |
| return; |
| } |
| BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser); |
| if (!browser_view) { |
| return; |
| } |
| browser_view->GetViewAccessibility().AnnounceText(text); |
| } |
| |
| base::WeakPtr<AutofillPopupView> PopupViewViews::CreateSubPopupView( |
| base::WeakPtr<AutofillSuggestionController> controller) { |
| if (GetWidget() && controller) { |
| return (new PopupViewViews( |
| static_cast<AutofillPopupController&>(*controller).GetWeakPtr(), |
| weak_ptr_factory_.GetWeakPtr(), GetWidget())) |
| ->GetWeakPtr(); |
| } |
| return nullptr; |
| } |
| |
| std::optional<AutofillClient::PopupScreenLocation> |
| PopupViewViews::GetPopupScreenLocation() const { |
| if (!GetWidget()) { |
| return std::nullopt; |
| } |
| |
| using ArrowPosition = AutofillClient::PopupScreenLocation::ArrowPosition; |
| auto convert_arrow_enum = |
| [](views::BubbleBorder::Arrow arrow) -> ArrowPosition { |
| switch (arrow) { |
| case views::BubbleBorder::Arrow::TOP_RIGHT: |
| return ArrowPosition::kTopRight; |
| case views::BubbleBorder::Arrow::TOP_LEFT: |
| return ArrowPosition::kTopLeft; |
| case views::BubbleBorder::Arrow::BOTTOM_RIGHT: |
| return ArrowPosition::kBottomRight; |
| case views::BubbleBorder::Arrow::BOTTOM_LEFT: |
| return ArrowPosition::kBottomLeft; |
| case views::BubbleBorder::Arrow::LEFT_TOP: |
| return ArrowPosition::kLeftTop; |
| case views::BubbleBorder::Arrow::RIGHT_TOP: |
| return ArrowPosition::kRightTop; |
| default: |
| NOTREACHED_NORETURN(); |
| } |
| }; |
| views::Border* border = GetWidget()->GetRootView()->GetBorder(); |
| CHECK(border); |
| |
| return AutofillClient::PopupScreenLocation{ |
| .bounds = GetWidget()->GetWindowBoundsInScreen(), |
| .arrow_position = convert_arrow_enum( |
| static_cast<views::BubbleBorder*>(border)->arrow())}; |
| } |
| |
| bool PopupViewViews::HasFocus() const { |
| if (!GetWidget()) { |
| return false; |
| } |
| |
| // The `CanActivate() && show_in_progress_` expression is needed to cover |
| // the case when this method is called during the `GetWidget()->Show()` |
| // execution and the popup is not yet active. It optimistically responds |
| // `true` and requires an additional `GetWidget()->IsActive()` check after |
| // the `GetWidget()->Show()` call to ensure the popup is shown successfully. |
| return (CanActivate() && show_in_progress_) || GetWidget()->IsActive(); |
| } |
| |
| void PopupViewViews::OnWidgetVisibilityChanged(views::Widget* widget, |
| bool visible) { |
| if (!visible || !controller_) { |
| return; |
| } |
| |
| Browser* browser = GetBrowser(); |
| if (!browser) { |
| return; |
| } |
| |
| // Show the in-product-help promo anchored to this bubble. |
| // The in-product-help promo is a bubble anchored to this row item to show |
| // educational messages. The promo bubble should only be shown once in one |
| // session and has a limit for how many times it can be shown at most in a |
| // period of time. |
| browser->window()->MaybeShowFeaturePromo( |
| feature_engagement::kIPHAutofillVirtualCardCVCSuggestionFeature); |
| browser->window()->MaybeShowFeaturePromo( |
| feature_engagement::kIPHAutofillVirtualCardSuggestionFeature); |
| browser->window()->MaybeShowFeaturePromo( |
| feature_engagement::kIPHAutofillExternalAccountProfileSuggestionFeature); |
| browser->window()->MaybeShowFeaturePromo( |
| feature_engagement::kIPHAutofillCreditCardBenefitFeature); |
| } |
| |
| bool PopupViewViews::SearchBarHandleKeyPressed(const ui::KeyEvent& event) { |
| if (!controller_) { |
| return false; |
| } |
| |
| // Handling events in the controller (the delegate's handler is prioritized by |
| // the search bar) enables keyboard navigation when the search bar input |
| // field is focused. |
| return controller_->HandleKeyPressEvent( |
| content::NativeWebKeyboardEvent(event)); |
| } |
| |
| void PopupViewViews::SetSelectedCell( |
| std::optional<CellIndex> cell_index, |
| PopupCellSelectionSource source, |
| AutoselectFirstSuggestion autoselect_first_suggestion, |
| bool suppress_popup) { |
| if (!controller_) { |
| return; |
| } |
| |
| std::optional<CellIndex> old_index = GetSelectedCell(); |
| if (old_index == cell_index) { |
| return; |
| } |
| |
| if (old_index) { |
| GetPopupRowViewAt(old_index->first).SetSelectedCell(std::nullopt); |
| } |
| |
| // New selected cell invalidates this scheduling (if it's running), cancel it. |
| open_sub_popup_timer_.Stop(); |
| |
| if (cell_index && HasPopupRowViewAt(cell_index->first)) { |
| has_keyboard_focus_ = true; |
| // The sub-popup hiding is canceled because the newly selected cell will |
| // rule the sub-pupop visibility from now. |
| no_selection_sub_popup_close_timer_.Stop(); |
| |
| row_with_selected_cell_ = cell_index->first; |
| PopupRowView& new_selected_row = GetPopupRowViewAt(cell_index->first); |
| new_selected_row.SetSelectedCell(cell_index->second); |
| new_selected_row.ScrollViewToVisible(); |
| |
| if (!controller_) { |
| // The previous SetSelectedCell() call may have hidden the popup. |
| return; |
| } |
| const Suggestion& suggestion = |
| controller_->GetSuggestionAt(cell_index->first); |
| |
| bool can_open_sub_popup = |
| !suppress_popup && |
| (cell_index->second == PopupRowView::CellType::kControl || |
| CanOpenSubPopupSuggestion(suggestion)); |
| |
| CHECK(!can_open_sub_popup || |
| !controller_->GetSuggestionAt(cell_index->first).children.empty()); |
| |
| std::optional<size_t> row_with_open_sub_popup = |
| can_open_sub_popup ? std::optional(cell_index->first) : std::nullopt; |
| base::TimeDelta delay = source == PopupCellSelectionSource::kMouse |
| ? kMouseOpenSubPopupDelay |
| : kNonMouseOpenSubPopupDelay; |
| open_sub_popup_timer_.Start( |
| FROM_HERE, delay, |
| base::BindOnce(&PopupViewViews::SetRowWithOpenSubPopup, |
| weak_ptr_factory_.GetWeakPtr(), row_with_open_sub_popup, |
| autoselect_first_suggestion)); |
| } else { |
| row_with_selected_cell_ = std::nullopt; |
| } |
| } |
| |
| bool PopupViewViews::HasPopupRowViewAt(size_t index) const { |
| return index < rows_.size() && |
| absl::holds_alternative<PopupRowView*>(rows_[index]); |
| } |
| |
| void PopupViewViews::InitViews(PopupViewSearchBarConfig search_bar_config) { |
| SetNotifyEnterExitOnChild(true); |
| SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical)); |
| |
| if (search_bar_config.enabled) { |
| search_bar_ = AddChildView(std::make_unique<PopupSearchBarView>( |
| search_bar_config.placeholder, |
| base::BindRepeating(&PopupViewViews::OnSearchBarInputChanged, |
| base::Unretained(this)), |
| base::BindRepeating(&PopupViewViews::OnSearchBarFocusLost, |
| base::Unretained(this)), |
| *this)); |
| search_bar_->SetProperty(views::kMarginsKey, |
| gfx::Insets::VH(GetContentsVerticalPadding(), 0)); |
| AddChildView(std::make_unique<PopupSeparatorView>(/*vertical_padding=*/0)); |
| } |
| |
| suggestions_container_ = |
| AddChildView(views::Builder<views::BoxLayoutView>() |
| .SetOrientation(views::BoxLayout::Orientation::kVertical) |
| .Build()); |
| |
| CreateSuggestionViews(); |
| } |
| |
| void PopupViewViews::CreateSuggestionViews() { |
| // Null all pointers prior to deleting the children views to avoid temporarily |
| // dangling pointers that might be picked up by dangle detection builds. Also, |
| // `footer_container_` is instantiated conditionally, which can make its value |
| // obsolete after `OnSuggestionsChanged()`. |
| scroll_view_ = nullptr; |
| body_container_ = nullptr; |
| footer_container_ = nullptr; |
| rows_.clear(); |
| suggestions_container_->RemoveAllChildViews(); |
| |
| const int kInterItemsPadding = GetContentsVerticalPadding(); |
| const std::vector<Suggestion> kSuggestions = controller_->GetSuggestions(); |
| |
| SetBackground( |
| views::CreateThemedSolidBackground(ui::kColorDropdownBackground)); |
| |
| rows_.reserve(kSuggestions.size()); |
| size_t current_line_number = 0u; |
| // TODO(b/325246516): Add "No suggestions found" label if there is a filter |
| // and there are only footer suggestions in the list. |
| // Add the body rows, if there are any. |
| if (!kSuggestions.empty() && !IsFooterItem(kSuggestions, 0u)) { |
| // Create a container to wrap the "regular" (non-footer) rows. |
| std::unique_ptr<views::BoxLayoutView> body_container = |
| views::Builder<views::BoxLayoutView>() |
| .SetOrientation(views::BoxLayout::Orientation::kVertical) |
| .SetInsideBorderInsets(gfx::Insets::VH(kInterItemsPadding, 0)) |
| .Build(); |
| |
| for (; current_line_number < kSuggestions.size() && |
| !IsFooterItem(kSuggestions, current_line_number); |
| ++current_line_number) { |
| switch (kSuggestions[current_line_number].type) { |
| case SuggestionType::kSeparator: |
| rows_.push_back(body_container->AddChildView( |
| std::make_unique<PopupSeparatorView>(kInterItemsPadding))); |
| break; |
| |
| case SuggestionType::kTitle: |
| rows_.push_back( |
| body_container->AddChildView(std::make_unique<PopupTitleView>( |
| kSuggestions[current_line_number].main_text.value))); |
| break; |
| |
| case SuggestionType::kMixedFormMessage: |
| case SuggestionType::kInsecureContextPaymentDisabledMessage: |
| rows_.push_back( |
| body_container->AddChildView(std::make_unique<PopupWarningView>( |
| kSuggestions[current_line_number]))); |
| break; |
| |
| // The default section contains all selectable rows and includes |
| // autocomplete, address, credit cards and passwords. |
| default: |
| std::optional<AutofillPopupController::SuggestionFilterMatch> |
| filter_match = |
| controller_->GetSuggestionFilterMatches().empty() |
| ? std::nullopt |
| : std::optional(controller_->GetSuggestionFilterMatches() |
| [current_line_number]); |
| PopupRowView* row_view = |
| body_container->AddChildView(CreatePopupRowView( |
| controller(), /*a11y_selection_delegate=*/*this, |
| /*selection_delegate=*/*this, current_line_number, |
| std::move(filter_match))); |
| rows_.push_back(row_view); |
| |
| const base::Feature* const feature_for_iph = |
| kSuggestions[current_line_number].feature_for_iph; |
| |
| // Set appropriate element ids for IPH targets, it is important to |
| // set them earlier to make sure the elements are discoverable later |
| // during popup's visibility change and the promo bubble showing. |
| if (feature_for_iph == |
| &feature_engagement::kIPHAutofillVirtualCardSuggestionFeature) { |
| row_view->SetProperty(views::kElementIdentifierKey, |
| kAutofillCreditCardSuggestionEntryElementId); |
| } else if (feature_for_iph == |
| &feature_engagement:: |
| kIPHAutofillVirtualCardCVCSuggestionFeature) { |
| row_view->SetProperty(views::kElementIdentifierKey, |
| kAutofillStandaloneCvcSuggestionElementId); |
| } else if (feature_for_iph == |
| &feature_engagement:: |
| kIPHAutofillExternalAccountProfileSuggestionFeature) { |
| row_view->SetProperty(views::kElementIdentifierKey, |
| kAutofillSuggestionElementId); |
| } else if (feature_for_iph == |
| &feature_engagement:: |
| kIPHAutofillCreditCardBenefitFeature) { |
| row_view->SetProperty(views::kElementIdentifierKey, |
| kAutofillCreditCardBenefitElementId); |
| } |
| } |
| } |
| |
| std::unique_ptr<views::ScrollView> scroll_view = |
| views::Builder<views::ScrollView>() |
| .SetBackgroundThemeColorId(ui::kColorDropdownBackground) |
| .SetHorizontalScrollBarMode( |
| views::ScrollView::ScrollBarMode::kDisabled) |
| .SetDrawOverflowIndicator(false) |
| .ClipHeightTo(0, body_container->GetPreferredSize().height()) |
| .Build(); |
| body_container_ = scroll_view->SetContents(std::move(body_container)); |
| scroll_view_ = suggestions_container_->AddChildView(std::move(scroll_view)); |
| suggestions_container_->SetFlexForView(scroll_view_.get(), 1); |
| } |
| |
| if (current_line_number >= kSuggestions.size()) { |
| return; |
| } |
| |
| auto footer_container = |
| views::Builder<views::BoxLayoutView>() |
| .SetOrientation(views::BoxLayout::Orientation::kVertical) |
| .SetBackground( |
| views::CreateThemedSolidBackground(ui::kColorDropdownBackground)) |
| .Build(); |
| |
| if (IsFooterScrollable()) { |
| footer_container_ = |
| body_container_->AddChildView(std::move(footer_container)); |
| } else { |
| // Add a separator between the main list of suggestions and the footer with |
| // no vertical padding as these elements have their own top/bottom paddings. |
| if (kSuggestions[current_line_number].type == SuggestionType::kSeparator) { |
| rows_.push_back(suggestions_container_->AddChildView( |
| std::make_unique<PopupSeparatorView>(/*vertical_padding=*/0))); |
| ++current_line_number; |
| } |
| |
| footer_container_ = |
| suggestions_container_->AddChildView(std::move(footer_container)); |
| footer_container_->SetInsideBorderInsets( |
| gfx::Insets::VH(kInterItemsPadding, 0)); |
| suggestions_container_->SetFlexForView(footer_container_, 0); |
| } |
| |
| for (; current_line_number < kSuggestions.size(); ++current_line_number) { |
| DCHECK(IsFooterItem(kSuggestions, current_line_number)); |
| // The footer can contain either footer views or separator lines. |
| if (kSuggestions[current_line_number].type == SuggestionType::kSeparator) { |
| rows_.push_back(footer_container_->AddChildView( |
| std::make_unique<PopupSeparatorView>(kInterItemsPadding))); |
| } else { |
| rows_.push_back(footer_container_->AddChildView(CreatePopupRowView( |
| controller(), /*a11y_selection_delegate=*/*this, |
| /*selection_delegate=*/*this, current_line_number))); |
| } |
| } |
| |
| // Adjust the scrollable area height. Make sure this adjustment always goes |
| // after changes that can affect `body_container_`'s size. |
| if (scroll_view_ && body_container_ && IsFooterScrollable()) { |
| scroll_view_->ClipHeightTo(0, body_container_->GetPreferredSize().height()); |
| } |
| } |
| |
| int PopupViewViews::AdjustWidth(int width) const { |
| if (width >= kAutofillPopupMaxWidth) { |
| return kAutofillPopupMaxWidth; |
| } |
| |
| if (width <= kAutofillPopupMinWidth) { |
| return kAutofillPopupMinWidth; |
| } |
| |
| // The popup size is being determined by the contents, rather than the min/max |
| // or the element bounds. Round up to a multiple of |
| // |kAutofillPopupWidthMultiple|. |
| if (width % kAutofillPopupWidthMultiple) { |
| width += |
| (kAutofillPopupWidthMultiple - (width % kAutofillPopupWidthMultiple)); |
| } |
| |
| return width; |
| } |
| |
| bool PopupViewViews::DoUpdateBoundsAndRedrawPopup() { |
| gfx::Size preferred_size = CalculatePreferredSize({}); |
| gfx::Rect popup_bounds; |
| |
| const gfx::Rect content_area_bounds = GetContentAreaBounds(); |
| // TODO(crbug.com/40799454) Once popups can render outside the main window on |
| // Linux, use the screen bounds. |
| const gfx::Rect top_window_bounds = GetTopWindowBounds(); |
| const gfx::Rect& max_bounds_for_popup = |
| PopupMayExceedContentAreaBounds(controller_->GetWebContents()) |
| ? top_window_bounds |
| : content_area_bounds; |
| |
| gfx::Rect element_bounds = |
| gfx::ToEnclosingRect(controller_->element_bounds()); |
| |
| // If the element exceeds the content area, ensure that the popup is still |
| // visually attached to the input element. |
| element_bounds.Intersect(content_area_bounds); |
| if (element_bounds.IsEmpty()) { |
| controller_->Hide(SuggestionHidingReason::kElementOutsideOfContentArea); |
| return false; |
| } |
| |
| // Consider the element is |kElementBorderPadding| pixels larger at the top |
| // and at the bottom in order to reposition the dropdown, so that it doesn't |
| // look too close to the element. |
| element_bounds.Inset( |
| gfx::Insets::VH(/*vertical=*/-kElementBorderPadding, /*horizontal=*/0)); |
| |
| if ((!body_container_ || body_container_->children().empty()) && |
| (!footer_container_ || footer_container_->children().empty())) { |
| controller_->Hide(SuggestionHidingReason::kNoSuggestions); |
| return false; |
| } |
| |
| if (!CanShowDropdownInBounds(max_bounds_for_popup)) { |
| controller_->Hide(SuggestionHidingReason::kInsufficientSpace); |
| return false; |
| } |
| |
| CalculatePopupYAndHeight(preferred_size.height(), max_bounds_for_popup, |
| element_bounds, &popup_bounds); |
| |
| // Adjust the width to compensate for a scroll bar, if necessary, and for |
| // other rules. |
| int scroll_width = 0; |
| if (scroll_view_ && preferred_size.height() > popup_bounds.height()) { |
| preferred_size.set_height(popup_bounds.height()); |
| |
| // Because the preferred size is greater than the bounds available, the |
| // contents will have to scroll. The scroll bar will steal width from the |
| // content and smoosh everything together. Instead, add to the width to |
| // compensate. |
| scroll_width = scroll_view_->GetScrollBarLayoutWidth(); |
| } |
| preferred_size.set_width(AdjustWidth(preferred_size.width() + scroll_width)); |
| |
| popup_bounds = GetOptionalPositionAndPlaceArrowOnPopup( |
| element_bounds, content_area_bounds, preferred_size); |
| |
| if (BoundsOverlapWithAnyOpenPrompt(popup_bounds, |
| controller_->GetWebContents())) { |
| controller_->Hide(SuggestionHidingReason::kOverlappingWithAnotherPrompt); |
| return false; |
| } |
| // On Windows, due to platform-specific implementation details, the previous |
| // check isn't reliable, and fails to detect open prompts. Since the most |
| // critical bubble is the permission bubble, we check for that specifically. |
| if (BoundsOverlapWithOpenPermissionsPrompt(popup_bounds, |
| controller_->GetWebContents())) { |
| controller_->Hide(SuggestionHidingReason::kOverlappingWithAnotherPrompt); |
| return false; |
| } |
| |
| // The pip surface is given the most preference while rendering. So, the |
| // autofill popup should not be shown when the picture in picture window |
| // hides the autofill form behind it. |
| // For more details on how this can happen, see crbug.com/1358647. |
| if (BoundsOverlapWithPictureInPictureWindow(popup_bounds)) { |
| controller_->Hide( |
| SuggestionHidingReason::kOverlappingWithPictureInPictureWindow); |
| return false; |
| } |
| |
| SetSize(preferred_size); |
| |
| popup_bounds.Inset(-GetWidget()->GetRootView()->GetInsets()); |
| GetWidget()->SetBounds(popup_bounds); |
| UpdateClipPath(); |
| |
| // If `kUiCompositorScrollWithLayers` is enabled, then a ScrollView performs |
| // scrolling by using layers. These layers are not affected by the clip path |
| // of the widget and their corners remain unrounded, thus going beyond |
| // the popup's rounded corners. To avoid these, set a corner radius for |
| // the ScrollView's ViewPort if layer scrolling is enabled. |
| if (scroll_view_ && |
| base::FeatureList::IsEnabled(::features::kUiCompositorScrollWithLayers)) { |
| scroll_view_->SetViewportRoundedCornerRadius( |
| gfx::RoundedCornersF(GetCornerRadius())); |
| } |
| |
| SchedulePaint(); |
| return true; |
| } |
| |
| void PopupViewViews::OnMouseEnteredInChildren() { |
| if (parent_ && parent_->get()) { |
| parent_->get()->OnMouseEnteredInChildren(); |
| } |
| |
| // Cancel scheluled sub-popup closing. |
| no_selection_sub_popup_close_timer_.Stop(); |
| } |
| |
| void PopupViewViews::OnMouseExitedInChildren() { |
| if (GetSelectedCell()) { |
| return; |
| } |
| |
| if (parent_ && parent_->get()) { |
| parent_->get()->OnMouseExitedInChildren(); |
| } |
| |
| // Schedule sub-popup closing. |
| no_selection_sub_popup_close_timer_.Start( |
| FROM_HERE, kNoSelectionHideSubPopupDelay, |
| base::BindRepeating(&PopupViewViews::SetRowWithOpenSubPopup, |
| weak_ptr_factory_.GetWeakPtr(), std::nullopt, |
| AutoselectFirstSuggestion(false))); |
| } |
| |
| bool PopupViewViews::IsFooterScrollable() const { |
| // Footer items of a root popup are expected to be more prioritized and |
| // therefore "sticky", i.e. not being scrollable with the whole popup content. |
| // `body_container_` is the container of regular suggestions, it must exist |
| // to place the footer there and thus make it scrollable too. |
| return parent_ && body_container_; |
| } |
| |
| bool PopupViewViews::CanShowDropdownInBounds(const gfx::Rect& bounds) const { |
| gfx::Rect element_bounds = |
| gfx::ToEnclosingRect(controller_->element_bounds()); |
| |
| // At least one suggestion and the sticky footer should be shown in the bounds |
| // of the content area so that the user notices the presence of the popup. |
| int min_height = 0; |
| if (body_container_ && !body_container_->children().empty()) { |
| min_height += body_container_->children()[0]->GetPreferredSize().height(); |
| } |
| if (search_bar_) { |
| min_height += search_bar_->GetPreferredSize().height(); |
| } |
| if (footer_container_ && !footer_container_->children().empty() && |
| !IsFooterScrollable()) { |
| // The footer is not scrollable, its full height should be considered. |
| min_height += footer_container_->GetPreferredSize().height(); |
| } |
| |
| return CanShowDropdownHere(min_height, bounds, element_bounds); |
| } |
| |
| void PopupViewViews::SetRowWithOpenSubPopup( |
| std::optional<size_t> row_index, |
| AutoselectFirstSuggestion autoselect_first_suggestion) { |
| if (!controller_) { |
| return; |
| } |
| |
| if (row_with_open_sub_popup_ == row_index) { |
| return; |
| } |
| |
| // Close previously open sub-popup if any. |
| if (row_with_open_sub_popup_ && |
| HasPopupRowViewAt(*row_with_open_sub_popup_)) { |
| controller_->HideSubPopup(); |
| GetPopupRowViewAt(*row_with_open_sub_popup_) |
| .SetChildSuggestionsDisplayed(false); |
| row_with_open_sub_popup_ = std::nullopt; |
| } |
| |
| // Open a sub-popup on the new cell if provided. |
| if (row_index && HasPopupRowViewAt(*row_index)) { |
| const Suggestion& suggestion = controller_->GetSuggestionAt(*row_index); |
| |
| CHECK(!suggestion.children.empty()); |
| |
| PopupRowView& row = GetPopupRowViewAt(*row_index); |
| if (controller_->OpenSubPopup(row.GetControlCellBounds(), |
| suggestion.children, |
| autoselect_first_suggestion)) { |
| row.SetChildSuggestionsDisplayed(true); |
| row_with_open_sub_popup_ = row_index; |
| if (autoselect_first_suggestion) { |
| row.SetSelectedCell(std::nullopt); |
| } |
| } |
| } |
| } |
| |
| bool PopupViewViews::CanOpenSubPopupSuggestion(const Suggestion& suggestion) { |
| // Checking both `is_acceptable` and `apply_deactivated_style` because the |
| // latter is used for disabling virtual cards which cannot open a sub popup. |
| return !suggestion.is_acceptable && !suggestion.apply_deactivated_style; |
| } |
| |
| void PopupViewViews::OnSearchBarInputChanged(const std::u16string& query) { |
| if (controller_) { |
| controller_->SetFilter( |
| query.empty() |
| ? std::nullopt |
| : std::optional(AutofillPopupController::SuggestionFilter(query))); |
| } |
| } |
| |
| bool PopupViewViews::SelectParentPopupContentCell() { |
| if (!row_with_open_sub_popup_) { |
| return false; |
| } |
| size_t row_index = *row_with_open_sub_popup_; |
| // Closing the sub-popup by setting `std::nullopt` is required as |
| // `suppress_popup=true` is not enough: the sub-popup closing will be |
| // prevented by the "same value" check. |
| SetRowWithOpenSubPopup(std::nullopt); |
| SetSelectedCell(CellIndex{row_index, PopupRowView::CellType::kContent}, |
| PopupCellSelectionSource::kKeyboard, |
| AutoselectFirstSuggestion(false), |
| /*suppress_popup=*/true); |
| return true; |
| } |
| |
| void PopupViewViews::OnSearchBarFocusLost() { |
| // Deactivate to ensure `HasFocus()` won't return `true`. |
| if (GetWidget()) { |
| GetWidget()->Deactivate(); |
| } |
| if (controller_) { |
| controller_->Hide(SuggestionHidingReason::kFocusChanged); |
| } |
| } |
| |
| base::WeakPtr<AutofillPopupView> PopupViewViews::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| BEGIN_METADATA(PopupViewViews) |
| END_METADATA |
| |
| // static |
| base::WeakPtr<AutofillPopupView> AutofillPopupView::Create( |
| base::WeakPtr<AutofillSuggestionController> controller) { |
| if (!controller || !CanShowRootPopup(*controller)) { |
| return nullptr; |
| } |
| |
| // On Desktop, all controllers are `AutofillPopupController`s. |
| return (new PopupViewViews( |
| static_cast<AutofillPopupController&>(*controller).GetWeakPtr())) |
| ->GetWeakPtr(); |
| } |
| |
| } // namespace autofill |