| // Copyright 2012 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/omnibox/omnibox_result_view.h" |
| |
| #include <limits.h> |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/ui/color/chrome_color_id.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/omnibox/omnibox_controller.h" |
| #include "chrome/browser/ui/omnibox/omnibox_edit_model.h" |
| #include "chrome/browser/ui/omnibox/omnibox_theme.h" |
| #include "chrome/browser/ui/views/location_bar/location_bar_view.h" |
| #include "chrome/browser/ui/views/location_bar/selected_keyword_view.h" |
| #include "chrome/browser/ui/views/omnibox/omnibox_local_answer_header_view.h" |
| #include "chrome/browser/ui/views/omnibox/omnibox_match_cell_view.h" |
| #include "chrome/browser/ui/views/omnibox/omnibox_popup_view_views.h" |
| #include "chrome/browser/ui/views/omnibox/omnibox_suggestion_button_row_view.h" |
| #include "chrome/browser/ui/views/omnibox/omnibox_text_view.h" |
| #include "chrome/browser/ui/views/omnibox/remove_suggestion_bubble.h" |
| #include "chrome/browser/ui/views/omnibox/rounded_omnibox_results_frame.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/omnibox/browser/actions/omnibox_pedal.h" |
| #include "components/omnibox/browser/autocomplete_match_type.h" |
| #include "components/omnibox/browser/omnibox.mojom-shared.h" |
| #include "components/omnibox/browser/omnibox_client.h" |
| #include "components/omnibox/browser/omnibox_popup_selection.h" |
| #include "components/omnibox/browser/vector_icons.h" |
| #include "components/omnibox/common/omnibox_features.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "third_party/metrics_proto/omnibox_event.pb.h" |
| #include "third_party/omnibox_proto/answer_data.pb.h" |
| #include "third_party/omnibox_proto/rich_answer_template.pb.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/color/color_id.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/rounded_corners_f.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/animation/ink_drop.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/button/image_button_factory.h" |
| #include "ui/views/controls/focus_ring.h" |
| #include "ui/views/controls/highlight_path_generator.h" |
| #include "ui/views/controls/link.h" |
| #include "ui/views/controls/separator.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/layout/flex_layout.h" |
| #include "ui/views/layout/layout_types.h" |
| #include "ui/views/metadata/type_conversion.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_class_properties.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "base/win/atl.h" |
| #endif |
| |
| namespace { |
| |
| bool PrefersHighContrast(const views::View* view) { |
| const ui::NativeTheme* const native_theme = view->GetNativeTheme(); |
| return native_theme && native_theme->preferred_contrast() == |
| ui::NativeTheme::PreferredContrast::kMore; |
| } |
| |
| class OmniboxResultViewButton : public views::ImageButton { |
| METADATA_HEADER(OmniboxResultViewButton, views::ImageButton) |
| |
| public: |
| OmniboxResultViewButton(int a11y_message_id, PressedCallback callback) |
| : ImageButton(std::move(callback)) { |
| views::ConfigureVectorImageButton(this); |
| |
| SetAnimationDuration(base::TimeDelta()); |
| views::InkDrop::Get(this)->GetInkDrop()->SetHoverHighlightFadeDuration( |
| base::TimeDelta()); |
| |
| SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); |
| |
| // Although this appears visually as a button, expose as a list box option |
| // so that it matches the other options within its list box container. |
| GetViewAccessibility().SetRole(ax::mojom::Role::kListBoxOption); |
| GetViewAccessibility().SetName(l10n_util::GetStringUTF16(a11y_message_id)); |
| } |
| }; |
| |
| BEGIN_METADATA(OmniboxResultViewButton) |
| END_METADATA |
| |
| constexpr float kIPHBackgroundBorderRadius = 8; |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // OmniboxResultSelectionIndicator |
| |
| class OmniboxResultSelectionIndicator : public views::View { |
| METADATA_HEADER(OmniboxResultSelectionIndicator, views::View) |
| |
| public: |
| const int kStrokeThickness = 4; |
| |
| OmniboxResultSelectionIndicator() { |
| // Height will automatically match the parent's height. |
| SetPreferredSize(gfx::Size(kStrokeThickness, 0)); |
| } |
| |
| // views::View: |
| void OnPaint(gfx::Canvas* canvas) override { |
| SkPath path = GetPath(); |
| cc::PaintFlags flags; |
| flags.setAntiAlias(true); |
| flags.setColor( |
| GetColorProvider()->GetColor(kColorOmniboxResultsFocusIndicator)); |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| canvas->DrawPath(path, flags); |
| } |
| |
| private: |
| // The focus bar is a straight vertical line with half-rounded endcaps. Since |
| // this geometry is nontrivial to represent using primitives, it's instead |
| // represented using a fill path. This matches the style and implementation |
| // used in Tab Groups. |
| SkPath GetPath() const { |
| SkPath path; |
| |
| path.moveTo(0, 0); |
| path.arcTo(kStrokeThickness, kStrokeThickness, 0, SkPath::kSmall_ArcSize, |
| SkPathDirection::kCW, kStrokeThickness, kStrokeThickness); |
| path.lineTo(kStrokeThickness, height() - kStrokeThickness); |
| path.arcTo(kStrokeThickness, kStrokeThickness, 0, SkPath::kSmall_ArcSize, |
| SkPathDirection::kCW, 0, height()); |
| path.close(); |
| |
| return path; |
| } |
| }; |
| |
| BEGIN_METADATA(OmniboxResultSelectionIndicator) |
| END_METADATA |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // OmniboxResultView, public: |
| |
| OmniboxResultView::OmniboxResultView(OmniboxPopupViewViews* popup_view, |
| size_t model_index) |
| : popup_view_(popup_view), |
| model_index_(model_index), |
| // Using base::Unretained is correct here. 'this' outlives the callback. |
| mouse_enter_exit_handler_( |
| base::BindRepeating(&OmniboxResultView::UpdateHoverState, |
| base::Unretained(this))) { |
| CHECK_GE(model_index, 0u); |
| |
| // The view hierarchy is: |
| // OmniboxResultView (FillLayout) |
| // selection_indicator_ |
| // local_answer_header_and_suggestion_and_buttons_ (BoxLayout vertical) |
| // local_answer_header_ (added lazily) |
| // divider_line_ |
| // suggestion_and_buttons (FlexLayout horizontal) |
| // suggestion_and_button_row (FlexLayout horizontal) |
| // suggestion_view_ |
| // button_row_ |
| // thumbs_up_button_ |
| // thumbs_down_button_ |
| // remove_suggestion_button_ |
| |
| // TODO(crbug.com/370088101): The division between `OmniboxResultView` and |
| // `suggestion_view_` is not clear. Should we inline `suggestion_view_`'s |
| // members in `OmniboxResultView`? Or should we move e.g. the thumb and |
| // remove buttons into `suggestion_view_`? `suggestion_view_` currently uses |
| // custom layout, so it's easier to add new views to `OmniboxResultView` |
| // when possible. |
| |
| SetLayoutManager(std::make_unique<views::FillLayout>()); |
| |
| selection_indicator_ = |
| AddChildView(std::make_unique<OmniboxResultSelectionIndicator>()); |
| |
| local_answer_header_and_suggestion_and_buttons_ = |
| AddChildView(std::make_unique<views::View>()); |
| local_answer_header_and_suggestion_and_buttons_ |
| ->SetLayoutManager(std::make_unique<views::BoxLayout>()) |
| ->SetOrientation(views::LayoutOrientation::kVertical); |
| |
| divider_line_ = local_answer_header_and_suggestion_and_buttons_->AddChildView( |
| std::make_unique<views::Separator>()); |
| divider_line_->SetOrientation(views::Separator::Orientation::kHorizontal); |
| divider_line_->SetProperty(views::kMarginsKey, gfx::Insets::TLBR(8, 0, 2, 0)); |
| |
| auto* suggestion_and_buttons = |
| local_answer_header_and_suggestion_and_buttons_->AddChildView( |
| std::make_unique<views::View>()); |
| suggestion_and_buttons |
| ->SetLayoutManager(std::make_unique<views::FlexLayout>()) |
| ->SetCrossAxisAlignment(views::LayoutAlignment::kCenter); |
| |
| views::View* suggestion_and_button_row = |
| suggestion_and_buttons->AddChildView(std::make_unique<views::View>()); |
| suggestion_and_button_row->SetLayoutManager( |
| std::make_unique<views::FlexLayout>()); |
| suggestion_and_button_row->SetProperty( |
| views::kFlexBehaviorKey, |
| // FlexSpecification has multiple constructors, and if no direction is |
| // specified, the settings will be used in both horizontal and vertical |
| // directions. Therefore, we must specify the horizontal direction. |
| // Otherwise, the vertical height will be stretched. |
| views::FlexSpecification(views::LayoutOrientation::kHorizontal, |
| views::MinimumFlexSizeRule::kScaleToZero, |
| views::MaximumFlexSizeRule::kUnbounded)); |
| |
| suggestion_view_ = suggestion_and_button_row->AddChildView( |
| std::make_unique<OmniboxMatchCellView>(this)); |
| suggestion_view_->iph_link_view()->SetCallback(base::BindRepeating( |
| &OmniboxResultView::OpenIphLink, weak_factory_.GetWeakPtr())); |
| |
| auto* const iph_link_focus_ring = |
| views::FocusRing::Get(suggestion_view_->iph_link_view()); |
| iph_link_focus_ring->SetHasFocusPredicate(base::BindRepeating( |
| [](const OmniboxResultView* result_view, const View* view) { |
| return view->GetVisible() && result_view->GetMatchSelected() && |
| result_view->popup_view_->GetSelection().state == |
| OmniboxPopupSelection::FOCUSED_IPH_LINK; |
| }, |
| base::Unretained(this))); |
| iph_link_focus_ring->SetColorId(kColorOmniboxResultsFocusIndicator); |
| |
| // TODO(b/345536738): Move the common code for setting up instances of |
| // OmniboxResultViewButton to the constructor. |
| thumbs_up_button_ = suggestion_and_buttons->AddChildView( |
| std::make_unique<OmniboxResultViewButton>( |
| IDS_ACC_THUMBS_UP_SUGGESTION_BUTTON, |
| base::BindRepeating( |
| &OmniboxResultView::ButtonPressed, base::Unretained(this), |
| OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_UP))); |
| thumbs_up_button_->SetProperty(views::kMarginsKey, |
| gfx::Insets::TLBR(0, 0, 0, 8)); |
| views::InstallCircleHighlightPathGenerator(thumbs_up_button_); |
| thumbs_up_button_->SetTooltipText( |
| l10n_util::GetStringUTF16(IDS_OMNIBOX_THUMBS_UP_SUGGESTION)); |
| auto* const thumbs_up_focus_ring = views::FocusRing::Get(thumbs_up_button_); |
| thumbs_up_focus_ring->SetHasFocusPredicate(base::BindRepeating( |
| [](const OmniboxResultView* results, const View* view) { |
| return view->GetVisible() && results->GetMatchSelected() && |
| (results->popup_view_->GetSelection().state == |
| OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_UP); |
| }, |
| base::Unretained(this))); |
| thumbs_up_focus_ring->SetColorId(kColorOmniboxResultsFocusIndicator); |
| |
| thumbs_down_button_ = suggestion_and_buttons->AddChildView( |
| std::make_unique<OmniboxResultViewButton>( |
| IDS_ACC_THUMBS_DOWN_SUGGESTION_BUTTON, |
| base::BindRepeating( |
| &OmniboxResultView::ButtonPressed, base::Unretained(this), |
| OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_DOWN))); |
| thumbs_down_button_->SetProperty(views::kMarginsKey, |
| gfx::Insets::TLBR(0, 0, 0, 8)); |
| views::InstallCircleHighlightPathGenerator(thumbs_down_button_); |
| thumbs_down_button_->SetTooltipText( |
| l10n_util::GetStringUTF16(IDS_OMNIBOX_THUMBS_DOWN_SUGGESTION)); |
| auto* const thumbs_down_focus_ring = |
| views::FocusRing::Get(thumbs_down_button_); |
| thumbs_down_focus_ring->SetHasFocusPredicate(base::BindRepeating( |
| [](const OmniboxResultView* results, const View* view) { |
| return view->GetVisible() && results->GetMatchSelected() && |
| (results->popup_view_->GetSelection().state == |
| OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_DOWN); |
| }, |
| base::Unretained(this))); |
| thumbs_down_focus_ring->SetColorId(kColorOmniboxResultsFocusIndicator); |
| |
| remove_suggestion_button_ = suggestion_and_buttons->AddChildView( |
| std::make_unique<OmniboxResultViewButton>( |
| IDS_ACC_REMOVE_SUGGESTION_BUTTON, |
| base::BindRepeating( |
| &OmniboxResultView::ButtonPressed, base::Unretained(this), |
| OmniboxPopupSelection::FOCUSED_BUTTON_REMOVE_SUGGESTION))); |
| remove_suggestion_button_->SetProperty(views::kMarginsKey, |
| gfx::Insets::TLBR(0, 0, 0, 16)); |
| views::InstallCircleHighlightPathGenerator(remove_suggestion_button_); |
| auto* const remove_focus_ring = |
| views::FocusRing::Get(remove_suggestion_button_); |
| remove_focus_ring->SetHasFocusPredicate(base::BindRepeating( |
| [](const OmniboxResultView* results, const View* view) { |
| return view->GetVisible() && results->GetMatchSelected() && |
| (results->popup_view_->GetSelection().state == |
| OmniboxPopupSelection::FOCUSED_BUTTON_REMOVE_SUGGESTION); |
| }, |
| base::Unretained(this))); |
| remove_focus_ring->SetColorId(kColorOmniboxResultsFocusIndicator); |
| |
| button_row_ = suggestion_and_button_row->AddChildView( |
| std::make_unique<OmniboxSuggestionButtonRowView>(popup_view_, |
| model_index)); |
| // If there's insufficient space for rendering both the suggestion text |
| // and the action chip row at their preferred sizes, the give priority to the |
| // button row by setting its order to 1 here and the suggestion view's order |
| // to 2 in `SetMatch()` (lower numbers get higher priority). |
| button_row_->SetProperty( |
| views::kFlexBehaviorKey, |
| views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero, |
| views::MaximumFlexSizeRule::kPreferred) |
| .WithOrder(1)); |
| |
| mouse_enter_exit_handler_.ObserveMouseEnterExitOn(this); |
| |
| GetViewAccessibility().SetRole(ax::mojom::Role::kListBoxOption); |
| UpdateAccessibleName(); |
| GetViewAccessibility().SetPosInSet(model_index_ + 1); |
| } |
| |
| OmniboxResultView::~OmniboxResultView() = default; |
| |
| // static |
| std::unique_ptr<views::Background> OmniboxResultView::GetPopupCellBackground( |
| const views::View* view, |
| OmniboxPartState part_state) { |
| DCHECK(view); |
| |
| // TODO(tapted): Consider using background()->SetNativeControlColor() and |
| // always have a background. |
| if (part_state == OmniboxPartState::NORMAL && !PrefersHighContrast(view)) { |
| return nullptr; |
| } |
| |
| if (part_state == OmniboxPartState::IPH) { |
| return views::CreateRoundedRectBackground( |
| GetOmniboxBackgroundColorId(part_state), |
| /*radius=*/kIPHBackgroundBorderRadius, |
| /*for_border_thickness=*/0); |
| } |
| |
| const float half_row_height = OmniboxMatchCellView::kRowHeight / 2; |
| gfx::RoundedCornersF radii = {0, half_row_height, half_row_height, 0}; |
| return views::CreateRoundedRectBackground( |
| GetOmniboxBackgroundColorId(part_state), radii); |
| } |
| |
| void OmniboxResultView::SetMatch(const AutocompleteMatch& match) { |
| match_ = match.GetMatchWithContentsAndDescriptionPossiblySwapped(); |
| |
| suggestion_view_->SetProperty(views::kMarginsKey, |
| gfx::Insets::TLBR(0, 0, 0, 0)); |
| // Allocate space for the suggestion text only after accounting for the space |
| // needed to render the inline action chip row, by setting the order to 2. |
| // |
| // In the toolbelt case, we want to snap the suggestion text to zero, since |
| // that looks better when there's not room for both. But in the normal case, |
| // we want to scale the suggestion text to zero since an elided suggestion |
| // still provides useful information. |
| suggestion_view_->SetProperty( |
| views::kFlexBehaviorKey, |
| views::FlexSpecification( |
| match_.IsToolbelt() ? |
| views::MinimumFlexSizeRule::kPreferredSnapToMinimum : |
| views::MinimumFlexSizeRule::kScaleToMinimum, |
| views::MaximumFlexSizeRule::kPreferred) |
| .WithOrder(2)); |
| |
| suggestion_view_->OnMatchUpdate(this, match_); |
| UpdateDividerLineVisibility(); |
| UpdateFeedbackButtonsVisibility(); |
| UpdateRemoveSuggestionVisibility(); |
| if (match_.IsIphSuggestion()) { |
| remove_suggestion_button_->SetTooltipText( |
| l10n_util::GetStringUTF16(IDS_OMNIBOX_CLOSE_IPH_SUGGESTION)); |
| remove_suggestion_button_->GetViewAccessibility().SetName( |
| l10n_util::GetStringUTF16(IDS_ACC_DISMISS_CHROME_TIP_BUTTON)); |
| } else { |
| remove_suggestion_button_->SetTooltipText( |
| l10n_util::GetStringUTF16(IDS_OMNIBOX_REMOVE_SUGGESTION)); |
| remove_suggestion_button_->GetViewAccessibility().SetName( |
| l10n_util::GetStringUTF16(IDS_ACC_REMOVE_SUGGESTION_BUTTON)); |
| } |
| |
| button_row_->UpdateFromModel(); |
| |
| if (match_.type == AutocompleteMatchType::Type::HISTORY_EMBEDDINGS_ANSWER) { |
| if (!local_answer_header_) { |
| local_answer_header_ = |
| local_answer_header_and_suggestion_and_buttons_->AddChildViewAt( |
| std::make_unique<OmniboxLocalAnswerHeaderView>(), 0); |
| } |
| local_answer_header_->SetText(match.history_embeddings_answer_header_text); |
| local_answer_header_->SetVisible(true); |
| local_answer_header_->SetThrobberVisibility( |
| match.history_embeddings_answer_header_loading); |
| } else if (local_answer_header_) { |
| local_answer_header_->SetVisible(false); |
| } |
| |
| ApplyThemeAndRefreshIcons(); |
| InvalidateLayout(); |
| UpdateAccessibleName(); |
| } |
| |
| void OmniboxResultView::ApplyThemeAndRefreshIcons(bool force_reapply_styles) { |
| const ui::ColorId icon_color_id = GetMatchSelected() |
| ? kColorOmniboxResultsIconSelected |
| : kColorOmniboxResultsIcon; |
| |
| // TODO(b/345536738): Iterate over all the buttons and updates their icons. |
| views::SetImageFromVectorIconWithColor( |
| thumbs_up_button_, |
| match_.feedback_type == FeedbackType::kThumbsUp |
| ? vector_icons::kThumbUpFilledIcon |
| : vector_icons::kThumbUpIcon, |
| GetLayoutConstant(LOCATION_BAR_ICON_SIZE), |
| GetColorProvider()->GetColor(icon_color_id), |
| /* omnibox buttons are never disabled */ |
| gfx::kPlaceholderColor); |
| if (thumbs_up_button_->GetVisible()) { |
| views::FocusRing::Get(thumbs_up_button_)->SchedulePaint(); |
| } |
| |
| views::SetImageFromVectorIconWithColor( |
| thumbs_down_button_, |
| match_.feedback_type == FeedbackType::kThumbsDown |
| ? vector_icons::kThumbDownFilledIcon |
| : vector_icons::kThumbDownIcon, |
| GetLayoutConstant(LOCATION_BAR_ICON_SIZE), |
| GetColorProvider()->GetColor(icon_color_id), |
| /* omnibox buttons are never disabled */ |
| gfx::kPlaceholderColor); |
| if (thumbs_down_button_->GetVisible()) { |
| views::FocusRing::Get(thumbs_down_button_)->SchedulePaint(); |
| } |
| |
| views::SetImageFromVectorIconWithColor( |
| remove_suggestion_button_, vector_icons::kCloseRoundedIcon, |
| GetLayoutConstant(LOCATION_BAR_ICON_SIZE), |
| GetColorProvider()->GetColor(icon_color_id), |
| /* omnibox buttons are never disabled */ |
| gfx::kPlaceholderColor); |
| if (remove_suggestion_button_->GetVisible()) { |
| views::FocusRing::Get(remove_suggestion_button_)->SchedulePaint(); |
| } |
| |
| const OmniboxPartState state = GetThemeState(); |
| SetBackground(GetPopupCellBackground(this, state)); |
| |
| // Reapply the dim color to account for the highlight state. |
| const bool selected = (state == OmniboxPartState::SELECTED); |
| const ui::ColorId dimmed_id = selected |
| ? kColorOmniboxResultsTextDimmedSelected |
| : kColorOmniboxResultsTextDimmed; |
| suggestion_view_->separator()->ApplyTextColor(dimmed_id); |
| |
| // Recreate the icons in case the color needs to change. |
| // Note: if this is an extension icon or favicon then this can be done in |
| // SetMatch() once (rather than repeatedly, as happens here). There may |
| // be an optimization opportunity here. |
| auto icon = GetIcon(); |
| if (icon.IsEmpty()) { |
| suggestion_view_->ClearIcon(); |
| } else { |
| suggestion_view_->SetIcon(*icon.ToImageSkia(), match_); |
| } |
| |
| // We must reapply colors for all the text fields here. If we don't, we can |
| // break theme changes for ZeroSuggest. See https://crbug.com/1095205. |
| // |
| // TODO(crbug.com/430318151): We should finish migrating this logic to live |
| // entirely within OmniboxTextView, which should keep track of its own |
| // OmniboxPart. |
| if (match_.type == AutocompleteMatchType::NULL_RESULT_MESSAGE) { |
| suggestion_view_->content()->ApplyTextColor( |
| match_.IsIphSuggestion() || match_.IsToolbelt() |
| ? kColorOmniboxResultsTextDimmed |
| : kColorOmniboxText); |
| } else if (force_reapply_styles || PrefersHighContrast(this)) { |
| // Normally, OmniboxTextView caches its appearance, but in high contrast, |
| // selected-ness changes the text colors, so the styling of the text part of |
| // the results needs to be recomputed. |
| suggestion_view_->content()->ReapplyStyling(); |
| suggestion_view_->description()->ReapplyStyling(); |
| } |
| |
| button_row_->SetThemeState(GetThemeState()); |
| |
| // The selection indicator indicates when the suggestion is focused. Do not |
| // show the selection indicator if an auxiliary button is selected. |
| if (match_.HasInstantKeyword( |
| popup_view_->controller()->client()->GetTemplateURLService())) { |
| const OmniboxPopupSelection::LineState line_state = |
| popup_view_->GetSelection().state; |
| selection_indicator_->SetVisible( |
| selected && |
| (line_state == OmniboxPopupSelection::LineState::NORMAL || |
| line_state == OmniboxPopupSelection::LineState::KEYWORD_MODE)); |
| } else { |
| selection_indicator_->SetVisible(selected && |
| popup_view_->GetSelection().state == |
| OmniboxPopupSelection::NORMAL); |
| } |
| |
| if (suggestion_view_->iph_link_view()->GetVisible()) { |
| views::FocusRing::Get(suggestion_view_->iph_link_view())->SchedulePaint(); |
| } |
| } |
| |
| void OmniboxResultView::OnSelectionStateChanged() { |
| UpdateDividerLineVisibility(); |
| UpdateFeedbackButtonsVisibility(); |
| UpdateRemoveSuggestionVisibility(); |
| UpdateAccessibleName(); |
| UpdateAccessibilitySelectedState(); |
| if (GetMatchSelected()) { |
| const auto selection_state = popup_view_->GetSelection().state; |
| |
| // The text is also accessible via text/value change events in the omnibox |
| // but this selection event allows the screen reader to get more details |
| // about the list and the user's position within it. |
| // Limit which selection states fire the events, in order to avoid duplicate |
| // events. Specifically, OmniboxPopupViewViews::ProvideButtonFocusHint() |
| // already fires the correct events when the user tabs to an attached button |
| // in the current row. |
| if (selection_state == OmniboxPopupSelection::NORMAL) { |
| popup_view_->FireAXEventsForNewActiveDescendant(this); |
| } |
| } |
| ApplyThemeAndRefreshIcons(); |
| button_row_->SelectionStateChanged(); |
| } |
| |
| bool OmniboxResultView::GetMatchSelected() const { |
| const auto selection = popup_view_->GetSelection(); |
| return selection.line == model_index_; |
| } |
| |
| views::Button* OmniboxResultView::GetActiveAuxiliaryButtonForAccessibility() { |
| return const_cast<views::Button*>( |
| std::as_const(*this).GetActiveAuxiliaryButtonForAccessibility()); |
| } |
| |
| const views::Button* |
| OmniboxResultView::GetActiveAuxiliaryButtonForAccessibility() const { |
| if (popup_view_->GetSelection().state == |
| OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_UP) { |
| return thumbs_up_button_; |
| } else if (popup_view_->GetSelection().state == |
| OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_DOWN) { |
| return thumbs_down_button_; |
| } else if (popup_view_->GetSelection().state == |
| OmniboxPopupSelection::FOCUSED_BUTTON_REMOVE_SUGGESTION) { |
| return remove_suggestion_button_; |
| } |
| |
| return button_row_->GetActiveButton(); |
| } |
| |
| OmniboxPartState OmniboxResultView::GetThemeState() const { |
| // NULL_RESULT_MESSAGE matches are no-op suggestions that only deliver a |
| // message. The selected and hovered states imply an action can be taken from |
| // that suggestion, so do not allow those states for this result. |
| if (match_.type == AutocompleteMatchType::NULL_RESULT_MESSAGE) { |
| if (match_.IsToolbelt()) { |
| return OmniboxPartState::TOOLBELT; |
| } |
| return match_.IsIphSuggestion() ? OmniboxPartState::IPH |
| : OmniboxPartState::NORMAL; |
| } |
| |
| if (GetMatchSelected()) { |
| return OmniboxPartState::SELECTED; |
| } |
| |
| // If we don't highlight the whole row when the user has the mouse over the |
| // remove suggestion button, it's unclear which suggestion is being removed. |
| return IsMouseHovered() ? OmniboxPartState::HOVERED |
| : OmniboxPartState::NORMAL; |
| } |
| |
| void OmniboxResultView::OnMatchIconUpdated() { |
| // The new icon will be fetched during ApplyThemeAndRefreshIcons(). |
| ApplyThemeAndRefreshIcons(); |
| } |
| |
| void OmniboxResultView::SetRichSuggestionImage(const gfx::ImageSkia& image) { |
| suggestion_view_->SetImage(image, match_); |
| } |
| |
| void OmniboxResultView::ButtonPressed(OmniboxPopupSelection::LineState state, |
| const ui::Event& event) { |
| popup_view_->model()->OpenSelection( |
| OmniboxPopupSelection(model_index_, state), event.time_stamp(), |
| WindowOpenDisposition::CURRENT_TAB, |
| /*via_keyboard=*/event.IsKeyEvent()); |
| if (state == OmniboxPopupSelection::FOCUSED_BUTTON_REMOVE_SUGGESTION) { |
| // The button could be pressed and the deletion successful, but the match |
| // may continue to appear with the X button remaining so it looked like it |
| // didn't delete. There may be a deeper async matches issue involved, but |
| // this seems to help in at least some cases (pedals + entities, e.g. dino). |
| UpdateRemoveSuggestionVisibility(); |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // OmniboxResultView, views::View overrides: |
| |
| bool OmniboxResultView::OnMousePressed(const ui::MouseEvent& event) { |
| if (event.IsOnlyLeftMouseButton()) { |
| popup_view_->SetSelectedIndex(model_index_); |
| // Inform the model that a new result is now selected via mouse press. |
| popup_view_->model()->OnNavigationLikely( |
| model_index_, omnibox::mojom::NavigationPredictor::kMouseDown); |
| } |
| return true; |
| } |
| |
| bool OmniboxResultView::OnMouseDragged(const ui::MouseEvent& event) { |
| if (HitTestPoint(event.location())) { |
| // When the drag enters or remains within the bounds of this view, either |
| // set the state to be selected or hovered, depending on the mouse button. |
| if (event.IsOnlyLeftMouseButton()) { |
| if (!GetMatchSelected()) { |
| popup_view_->SetSelectedIndex(model_index_); |
| } |
| } else { |
| UpdateHoverState(); |
| } |
| return true; |
| } |
| |
| // When the drag leaves the bounds of this view, cancel the hover state and |
| // pass control to the popup view. |
| UpdateHoverState(); |
| SetMouseAndGestureHandler(popup_view_); |
| return false; |
| } |
| |
| void OmniboxResultView::OnMouseReleased(const ui::MouseEvent& event) { |
| if (AutocompleteMatch::IsFeaturedSearchType(match_.type)) { |
| // Featured search matches in the keyword mode refresh are a special case |
| // that does not commit the omnibox by opening a selected match. |
| OmniboxEditModel* model = popup_view_->model(); |
| model->ClearKeyword(); |
| model->SetPopupSelection(OmniboxPopupSelection( |
| model_index_, OmniboxPopupSelection::LineState::KEYWORD_MODE)); |
| model->AcceptKeyword(metrics::OmniboxEventProto::TAB); |
| return; |
| } |
| |
| if (event.IsOnlyMiddleMouseButton() || event.IsOnlyLeftMouseButton()) { |
| const auto disposition = event.IsOnlyLeftMouseButton() |
| ? WindowOpenDisposition::CURRENT_TAB |
| : WindowOpenDisposition::NEW_BACKGROUND_TAB; |
| popup_view_->model()->OpenSelection(OmniboxPopupSelection(model_index_), |
| event.time_stamp(), disposition); |
| } |
| } |
| |
| void OmniboxResultView::OnMouseEntered(const ui::MouseEvent& event) { |
| UpdateHoverState(); |
| } |
| |
| void OmniboxResultView::OnMouseExited(const ui::MouseEvent& event) { |
| UpdateHoverState(); |
| } |
| |
| void OmniboxResultView::OnThemeChanged() { |
| views::View::OnThemeChanged(); |
| ApplyThemeAndRefreshIcons(/*force_reapply_styles=*/true); |
| } |
| |
| void OmniboxResultView::UpdateAccessibilityProperties() { |
| GetViewAccessibility().SetSetSize( |
| popup_view_->controller()->autocomplete_controller()->result().size()); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // OmniboxResultView, private: |
| |
| void OmniboxResultView::OpenIphLink() { |
| popup_view_->controller()->client()->OpenIphLink(match_.iph_link_url); |
| } |
| |
| gfx::Image OmniboxResultView::GetIcon() const { |
| // Usually, use kColorOmniboxResultsIcon[Selected] for icon color. Except for |
| // history cluster suggestions which want to stand out. They reuse the |
| // kColorOmniboxResultsUrl[Selected] color which is intended for the URL text |
| // in suggestion texts. |
| ui::ColorId vector_icon_color_id; |
| if (match_.type == AutocompleteMatchType::STARTER_PACK || |
| match_.type == AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH) { |
| vector_icon_color_id = kColorOmniboxResultsStarterPackIcon; |
| } else if (match_.type == AutocompleteMatchType::HISTORY_CLUSTER || |
| match_.type == AutocompleteMatchType::PEDAL) { |
| vector_icon_color_id = kColorOmniboxAnswerIconGM3Foreground; |
| } else { |
| vector_icon_color_id = GetMatchSelected() ? kColorOmniboxResultsIconSelected |
| : kColorOmniboxResultsIcon; |
| } |
| |
| return popup_view_->GetMatchIcon( |
| match_, GetColorProvider()->GetColor(vector_icon_color_id)); |
| } |
| |
| void OmniboxResultView::UpdateHoverState() { |
| UpdateDividerLineVisibility(); |
| UpdateFeedbackButtonsVisibility(); |
| UpdateRemoveSuggestionVisibility(); |
| ApplyThemeAndRefreshIcons(); |
| GetViewAccessibility().SetIsHovered(IsMouseHovered()); |
| } |
| |
| void OmniboxResultView::UpdateDividerLineVisibility() { |
| const bool old_visibility = divider_line_->GetVisible(); |
| const bool new_visibility = match_.IsToolbelt(); |
| |
| divider_line_->SetVisible(new_visibility); |
| |
| if (old_visibility != new_visibility) { |
| InvalidateLayout(); |
| } |
| } |
| |
| void OmniboxResultView::UpdateFeedbackButtonsVisibility() { |
| const bool old_visibility = thumbs_up_button_->GetVisible(); |
| const bool new_visibility = |
| popup_view_->model()->IsPopupControlPresentOnMatch(OmniboxPopupSelection( |
| model_index_, OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_UP)) && |
| (GetMatchSelected() || IsMouseHovered()); |
| |
| // Same rules apply to both buttons. |
| thumbs_up_button_->SetVisible(new_visibility); |
| thumbs_down_button_->SetVisible(new_visibility); |
| |
| if (old_visibility != new_visibility) { |
| InvalidateLayout(); |
| } |
| } |
| |
| // TODO(b/345536738): Introduce a single UpdateButtonsVisibility() that iterates |
| // over all the buttons and updates their visibilities. |
| void OmniboxResultView::UpdateRemoveSuggestionVisibility() { |
| const bool old_visibility = remove_suggestion_button_->GetVisible(); |
| const bool new_visibility = |
| popup_view_->model()->IsPopupControlPresentOnMatch(OmniboxPopupSelection( |
| model_index_, |
| OmniboxPopupSelection::FOCUSED_BUTTON_REMOVE_SUGGESTION)) && |
| (GetMatchSelected() || IsMouseHovered()); |
| |
| remove_suggestion_button_->SetVisible(new_visibility); |
| |
| if (old_visibility != new_visibility) { |
| InvalidateLayout(); |
| } |
| } |
| |
| void OmniboxResultView::UpdateAccessibilitySelectedState() { |
| GetViewAccessibility().SetIsSelected(GetMatchSelected()); |
| } |
| |
| void OmniboxResultView::UpdateAccessibleName() { |
| // Get the label without the ", n of m" positional text appended. |
| // The positional info is provided via |
| // ax::mojom::IntAttribute::kPosInSet/SET_SIZE and providing it via text as |
| // well would result in duplicate announcements. |
| |
| const auto* autocomplete_controller = |
| popup_view_->controller()->autocomplete_controller(); |
| |
| // TODO(tommycli): We re-fetch the original match from the popup model, |
| // because |match_| already has its contents and description swapped by this |
| // class, and we don't want that for the bubble. We should improve this. |
| const bool is_selected = GetMatchSelected(); |
| if (model_index_ < autocomplete_controller->result().size()) { |
| const auto raw_match = |
| autocomplete_controller->result().match_at(model_index_); |
| // The selected match can have a special name, e.g. when is one or more |
| // buttons that can be tabbed to. |
| std::u16string label; |
| if (is_selected) { |
| // The selected match can have a special name, e.g. when is one or more |
| // buttons that can be tabbed to. |
| label = |
| popup_view_->model()->GetPopupAccessibilityLabelForCurrentSelection( |
| raw_match.contents, false); |
| |
| // If the line immediately after the current selection is the |
| // informational IPH row, append its accessibility label at the end of |
| // this selection's accessibility label. |
| label += popup_view_->model() |
| ->MaybeGetPopupAccessibilityLabelForIPHSuggestion(); |
| } else { |
| label = AutocompleteMatchType::ToAccessibilityLabel( |
| raw_match, |
| popup_view_->model()->GetSuggestionGroupHeaderText( |
| raw_match.suggestion_group_id), |
| raw_match.contents); |
| } |
| GetViewAccessibility().SetName(label); |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // OmniboxResultView, views::View overrides, private: |
| |
| void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) { |
| InvalidateLayout(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // OmniboxResultView, overrides, private: |
| |
| DEFINE_ENUM_CONVERTERS(OmniboxPartState, |
| {OmniboxPartState::NORMAL, u"NORMAL"}, |
| {OmniboxPartState::HOVERED, u"HOVERED"}, |
| {OmniboxPartState::SELECTED, u"SELECTED"}) |
| |
| BEGIN_METADATA(OmniboxResultView) |
| ADD_READONLY_PROPERTY_METADATA(bool, MatchSelected) |
| ADD_READONLY_PROPERTY_METADATA(OmniboxPartState, ThemeState) |
| ADD_READONLY_PROPERTY_METADATA(gfx::Image, Icon) |
| END_METADATA |