blob: ea9037ce50dd291468e40158e51e94a0346c4d7d [file] [log] [blame]
// 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