blob: 7515e27eb1ea21a34c178c33ad577537553e4f17 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/omnibox/omnibox_header_view.h"
#include "base/functional/bind.h"
#include "base/i18n/case_conversion.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/omnibox/omnibox_theme.h"
#include "chrome/browser/ui/views/chrome_typography.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_result_view.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_popup_selection.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/strings/grit/components_strings.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/color/color_provider.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/controls/button/button.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/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
OmniboxHeaderView::OmniboxHeaderView(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(&OmniboxHeaderView::UpdateUI,
base::Unretained(this))) {
views::BoxLayout* layout =
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
// This is the designer-provided spacing that matches the NTP Realbox.
// TODO(khalidpeer): Update this spacing for realbox per CR23 guidelines.
const int spacing_between_label_and_icon =
OmniboxFieldTrial::IsChromeRefreshSuggestIconsEnabled() ? 0 : 8;
layout->set_between_child_spacing(spacing_between_label_and_icon);
header_label_ = AddChildView(std::make_unique<views::Label>());
header_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
const gfx::FontList& font =
views::TypographyProvider::Get()
.GetFont(CONTEXT_OMNIBOX_SECTION_HEADER, views::style::STYLE_PRIMARY)
.DeriveWithWeight(gfx::Font::Weight::MEDIUM);
header_label_->SetFontList(font);
header_toggle_button_ = AddChildView(views::CreateVectorToggleImageButton(
base::BindRepeating(&OmniboxHeaderView::HeaderToggleButtonPressed,
base::Unretained(this))));
mouse_enter_exit_handler_.ObserveMouseEnterExitOn(header_toggle_button_);
views::InstallCircleHighlightPathGenerator(header_toggle_button_);
header_toggle_button_->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
views::FocusRing::Install(header_toggle_button_);
views::FocusRing::Get(header_toggle_button_)
->SetHasFocusPredicate(base::BindRepeating(
[](const OmniboxHeaderView* header, const View* view) {
return view->GetVisible() &&
header->popup_view_->model()->GetPopupSelection() ==
header->GetHeaderSelection();
},
base::Unretained(this)));
views::FocusRing::Get(header_toggle_button_)
->SetOutsetFocusRingDisabled(true);
}
void OmniboxHeaderView::SetHeader(const std::u16string& header_text,
bool is_suggestion_group_hidden) {
header_text_ = header_text;
// TODO(tommycli): Our current design calls for uppercase text here, but
// it seems like an open question what should happen for non-Latin locales.
// Moreover, it seems unusual to do case conversion in Views in general.
std::u16string header_str = header_text_;
if (!OmniboxFieldTrial::IsChromeRefreshSuggestIconsEnabled()) {
header_str = base::i18n::ToUpper(header_str);
}
header_label_->SetText(header_str);
header_toggle_button_->SetToggled(is_suggestion_group_hidden);
}
gfx::Insets OmniboxHeaderView::GetInsets() const {
// Makes the header height roughly the same as the single-line row height.
const int vertical =
OmniboxFieldTrial::IsChromeRefreshSuggestIconsEnabled() ? 8 : 6;
// Aligns the header text with the icons of ordinary matches. The assumed
// small icon width here is lame, but necessary, since it's not explicitly
// defined anywhere else in the code.
constexpr int assumed_match_cell_icon_width = 16;
constexpr int left_inset = OmniboxMatchCellView::kMarginLeft +
(OmniboxMatchCellView::kImageBoundsWidth -
assumed_match_cell_icon_width) /
2;
return gfx::Insets::TLBR(vertical, left_inset, vertical,
OmniboxMatchCellView::kMarginRight);
}
bool OmniboxHeaderView::OnMousePressed(const ui::MouseEvent& event) {
// Needed to receive the OnMouseReleased event.
return true;
}
void OmniboxHeaderView::OnMouseReleased(const ui::MouseEvent& event) {
popup_view_->model()->OpenSelection(GetHeaderSelection(), event.time_stamp());
}
void OmniboxHeaderView::OnMouseEntered(const ui::MouseEvent& event) {
UpdateUI();
}
void OmniboxHeaderView::OnMouseExited(const ui::MouseEvent& event) {
UpdateUI();
}
void OmniboxHeaderView::OnThemeChanged() {
views::View::OnThemeChanged();
// When the theme is updated, also refresh the hover-specific UI, which is
// all of the UI.
UpdateUI();
}
void OmniboxHeaderView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
// Hidden OmniboxHeaderView instances are not associated with any group ID, so
// they are neither collapsed or expanded.s
if (!GetVisible()) {
return;
}
node_data->AddState(suggestion_group_hidden_ ? ax::mojom::State::kCollapsed
: ax::mojom::State::kExpanded);
}
void OmniboxHeaderView::UpdateUI() {
OmniboxPartState part_state = OmniboxPartState::NORMAL;
if (popup_view_->model()->GetPopupSelection() == GetHeaderSelection()) {
part_state = OmniboxPartState::SELECTED;
} else if (IsMouseHovered()) {
part_state = OmniboxPartState::HOVERED;
}
const auto* const color_provider = GetColorProvider();
const SkColor text_color =
color_provider->GetColor((part_state == OmniboxPartState::SELECTED)
? kColorOmniboxResultsTextDimmedSelected
: kColorOmniboxResultsTextDimmed);
header_label_->SetEnabledColor(text_color);
const SkColor icon_color =
color_provider->GetColor((part_state == OmniboxPartState::SELECTED)
? kColorOmniboxResultsIconSelected
: kColorOmniboxResultsIcon);
views::InkDrop::Get(header_toggle_button_)->SetBaseColor(icon_color);
int dip_size = GetLayoutConstant(LOCATION_BAR_ICON_SIZE);
const gfx::ImageSkia arrow_down = gfx::CreateVectorIcon(
OmniboxFieldTrial::IsChromeRefreshSuggestIconsEnabled()
? omnibox::kArrowDownChromeRefreshIcon
: omnibox::kChevronIcon,
dip_size, icon_color);
const ui::ImageModel arrow_up =
OmniboxFieldTrial::IsChromeRefreshSuggestIconsEnabled()
? ui::ImageModel::FromVectorIcon(omnibox::kArrowUpChromeRefreshIcon,
icon_color, dip_size)
: ui::ImageModel::FromImageSkia(
gfx::ImageSkiaOperations::CreateRotatedImage(
arrow_down, SkBitmapOperations::ROTATION_180_CW));
// The "untoggled" button state corresponds with the group being shown.
// The button's action is therefore to Hide the group, when clicked.
header_toggle_button_->SetImageModel(views::Button::STATE_NORMAL, arrow_up);
header_toggle_button_->SetTooltipText(
l10n_util::GetStringUTF16(IDS_TOOLTIP_HEADER_HIDE_SUGGESTIONS_BUTTON));
header_toggle_button_->SetAccessibleName(l10n_util::GetStringFUTF16(
IDS_ACC_HEADER_HIDE_SUGGESTIONS_BUTTON, header_text_));
// The "toggled" button state corresponds with the group being hidden.
// The button's action is therefore to Show the group, when clicked.
header_toggle_button_->SetToggledImageModel(
views::Button::STATE_NORMAL, ui::ImageModel::FromImageSkia(arrow_down));
header_toggle_button_->SetToggledTooltipText(
l10n_util::GetStringUTF16(IDS_TOOLTIP_HEADER_SHOW_SUGGESTIONS_BUTTON));
header_toggle_button_->SetToggledAccessibleName(l10n_util::GetStringFUTF16(
IDS_ACC_HEADER_SHOW_SUGGESTIONS_BUTTON, header_text_));
views::FocusRing::Get(header_toggle_button_)->SchedulePaint();
// It's a little hokey that we're stealing the logic for the background
// color from OmniboxResultView. If we start doing this is more than just
// one place, we should introduce a more elegant abstraction here.
if (!OmniboxFieldTrial::IsChromeRefreshSuggestIconsEnabled()) {
SetBackground(OmniboxResultView::GetPopupCellBackground(this, part_state));
}
}
void OmniboxHeaderView::HeaderToggleButtonPressed() {
popup_view_->model()->OpenSelection(GetHeaderSelection(), base::TimeTicks());
// The PrefChangeRegistrar will update the actual button toggle state.
}
void OmniboxHeaderView::SetSuggestionGroupVisibility(
bool suggestion_group_hidden) {
suggestion_group_hidden_ = suggestion_group_hidden;
NotifyAccessibilityEvent(ax::mojom::Event::kExpandedChanged, true);
// Because this view doesn't have true focus (it stays on the textfield),
// we also need to manually announce state changes.
GetViewAccessibility().AnnounceText(l10n_util::GetStringFUTF16(
suggestion_group_hidden_ ? IDS_ACC_HEADER_SECTION_HIDDEN
: IDS_ACC_HEADER_SECTION_SHOWN,
header_text_));
header_toggle_button_->SetToggled(suggestion_group_hidden_);
}
// Convenience method to get the OmniboxPopupSelection for this view.
OmniboxPopupSelection OmniboxHeaderView::GetHeaderSelection() const {
return OmniboxPopupSelection(model_index_,
OmniboxPopupSelection::FOCUSED_BUTTON_HEADER);
}
BEGIN_METADATA(OmniboxHeaderView)
ADD_READONLY_PROPERTY_METADATA(OmniboxPopupSelection, HeaderSelection)
END_METADATA