blob: 9d4b2e3fb558d7e6e2b62f3b847446a803258535 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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_row_view.h"
#include "base/bind.h"
#include "base/i18n/case_conversion.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ui/color/chrome_color_id.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_mouse_enter_exit_handler.h"
#include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_popup_selection.h"
#include "components/omnibox/browser/omnibox_prefs.h"
#include "components/omnibox/browser/suggestion_group.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.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_header_macros.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/metadata/type_conversion.h"
#include "ui/views/style/typography.h"
class OmniboxRowView::HeaderView : public views::View {
public:
METADATA_HEADER(HeaderView);
explicit HeaderView(OmniboxRowView* row_view)
: row_view_(row_view),
// Using base::Unretained is correct here. 'this' outlives the callback.
mouse_enter_exit_handler_(base::BindRepeating(&HeaderView::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.
layout->set_between_child_spacing(8);
header_label_ = AddChildView(std::make_unique<views::Label>());
header_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
const gfx::FontList& font =
views::style::GetFont(CONTEXT_OMNIBOX_PRIMARY,
views::style::STYLE_PRIMARY)
.DeriveWithWeight(gfx::Font::Weight::MEDIUM);
header_label_->SetFontList(font);
header_toggle_button_ =
AddChildView(views::CreateVectorToggleImageButton(base::BindRepeating(
&HeaderView::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([&](View* view) {
return view->GetVisible() &&
row_view_->model_->GetPopupSelection() == GetHeaderSelection();
});
if (row_view_->pref_service_) {
pref_change_registrar_.Init(row_view_->pref_service_);
// Unretained is appropriate here. 'this' will outlive the registrar.
pref_change_registrar_.Add(omnibox::kSuggestionGroupVisibility,
base::BindRepeating(&HeaderView::OnPrefChanged,
base::Unretained(this)));
}
}
void SetHeader(SuggestionGroupId suggestion_group_id,
const std::u16string& header_text) {
suggestion_group_id_ = suggestion_group_id;
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.
header_label_->SetText(base::i18n::ToUpper(header_text_));
if (row_view_->pref_service_) {
suggestion_group_hidden_ =
row_view_->model_->result().IsSuggestionGroupHidden(
row_view_->pref_service_, suggestion_group_id_);
header_toggle_button_->SetToggled(suggestion_group_hidden_);
}
}
// views::View:
gfx::Insets GetInsets() const override {
// Makes the header height roughly the same as the single-line row height.
constexpr int vertical = 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 OnMousePressed(const ui::MouseEvent& event) override {
// Needed to receive the OnMouseReleased event.
return true;
}
void OnMouseReleased(const ui::MouseEvent& event) override {
row_view_->model_->TriggerPopupSelectionAction(GetHeaderSelection());
}
void OnMouseEntered(const ui::MouseEvent& event) override { UpdateUI(); }
void OnMouseExited(const ui::MouseEvent& event) override { UpdateUI(); }
void OnThemeChanged() override {
views::View::OnThemeChanged();
// When the theme is updated, also refresh the hover-specific UI, which is
// all of the UI.
UpdateUI();
}
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
// Hidden HeaderView 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);
}
// Updates the UI state for the new hover or selection state.
void UpdateUI() {
OmniboxPartState part_state = OmniboxPartState::NORMAL;
if (row_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(omnibox::kChevronIcon, dip_size, icon_color);
const gfx::ImageSkia arrow_up =
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_->SetImage(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_->SetToggledImage(views::Button::STATE_NORMAL,
&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.
SetBackground(OmniboxResultView::GetPopupCellBackground(this, part_state));
}
views::Button* header_toggle_button() const { return header_toggle_button_; }
private:
void HeaderToggleButtonPressed() {
row_view_->model_->TriggerPopupSelectionAction(GetHeaderSelection());
// The PrefChangeRegistrar will update the actual button toggle state.
}
// Updates the hide button's toggle state.
void OnPrefChanged() {
DCHECK(row_view_->pref_service_);
bool was_hidden = suggestion_group_hidden_;
suggestion_group_hidden_ =
row_view_->model_->result().IsSuggestionGroupHidden(
row_view_->pref_service_, suggestion_group_id_);
if (was_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 GetHeaderSelection() const {
return OmniboxPopupSelection(row_view_->line_,
OmniboxPopupSelection::FOCUSED_BUTTON_HEADER);
}
// Non-owning pointer our parent row view. We access a lot of private members
// of our outer class. This lets us save quite a bit of state duplication.
const raw_ptr<OmniboxRowView> row_view_;
// The Label containing the header text. This is never nullptr.
raw_ptr<views::Label> header_label_;
// The button used to toggle hiding suggestions with this header.
raw_ptr<views::ToggleImageButton> header_toggle_button_;
// The group ID associated with this header.
SuggestionGroupId suggestion_group_id_ = SuggestionGroupId::kInvalid;
// The unmodified header text for this header.
std::u16string header_text_;
// Stores whether or not the group was hidden. This is used to fire correct
// accessibility change events.
bool suggestion_group_hidden_ = false;
// A pref change registrar for toggling the toggle button's state. This is
// needed because the preference state can change through multiple UIs.
PrefChangeRegistrar pref_change_registrar_;
// Keeps track of mouse-enter and mouse-exit events of child Views.
OmniboxMouseEnterExitHandler mouse_enter_exit_handler_;
};
DEFINE_ENUM_CONVERTERS(OmniboxPopupSelection::LineState,
{OmniboxPopupSelection::FOCUSED_BUTTON_HEADER,
u"FOCUSED_BUTTON_HEADER"},
{OmniboxPopupSelection::NORMAL, u"NORMAL"},
{OmniboxPopupSelection::KEYWORD_MODE, u"KEYWORD_MODE"},
{OmniboxPopupSelection::FOCUSED_BUTTON_TAB_SWITCH,
u"FOCUSED_BUTTON_TAB_SWITCH"},
{OmniboxPopupSelection::FOCUSED_BUTTON_ACTION,
u"FOCUSED_BUTTON_ACTION"},
{OmniboxPopupSelection::FOCUSED_BUTTON_REMOVE_SUGGESTION,
u"FOCUSED_BUTTON_REMOVE_SUGGESTION"})
template <>
struct ui::metadata::TypeConverter<OmniboxPopupSelection>
: public ui::metadata::BaseTypeConverter<true> {
static std::u16string ToString(
ui::metadata::ArgType<OmniboxPopupSelection> source_value);
static absl::optional<OmniboxPopupSelection> FromString(
const std::u16string& source_value);
static ui::metadata::ValidStrings GetValidStrings() { return {}; }
};
// static
std::u16string ui::metadata::TypeConverter<OmniboxPopupSelection>::ToString(
ui::metadata::ArgType<OmniboxPopupSelection> source_value) {
return u"{" + base::NumberToString16(source_value.line) + u"," +
TypeConverter<OmniboxPopupSelection::LineState>::ToString(
source_value.state) +
u"}";
}
// static
absl::optional<OmniboxPopupSelection> ui::metadata::TypeConverter<
OmniboxPopupSelection>::FromString(const std::u16string& source_value) {
const auto values = base::SplitString(
source_value, u"{,}", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (values.size() != 2)
return absl::nullopt;
// TODO(pkasting): This should be size_t, but for some reason that won't link
// on Mac.
const absl::optional<uint32_t> line =
TypeConverter<uint32_t>::FromString(values[0]);
const absl::optional<OmniboxPopupSelection::LineState> state =
TypeConverter<OmniboxPopupSelection::LineState>::FromString(values[1]);
return (line.has_value() && state.has_value())
? absl::make_optional<OmniboxPopupSelection>(line.value(),
state.value())
: absl::nullopt;
}
BEGIN_METADATA(OmniboxRowView, HeaderView, views::View)
ADD_READONLY_PROPERTY_METADATA(OmniboxPopupSelection, HeaderSelection)
END_METADATA
OmniboxRowView::OmniboxRowView(size_t line,
OmniboxEditModel* model,
std::unique_ptr<OmniboxResultView> result_view,
PrefService* pref_service)
: line_(line), model_(model), pref_service_(pref_service) {
DCHECK(result_view);
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
result_view_ = AddChildView(std::move(result_view));
}
void OmniboxRowView::ShowHeader(SuggestionGroupId suggestion_group_id,
const std::u16string& header_text) {
// Create the header (at index 0) if it doesn't exist.
if (header_view_ == nullptr)
header_view_ = AddChildViewAt(std::make_unique<HeaderView>(this), 0);
header_view_->SetHeader(suggestion_group_id, header_text);
header_view_->SetVisible(true);
}
void OmniboxRowView::HideHeader() {
if (header_view_)
header_view_->SetVisible(false);
}
void OmniboxRowView::OnSelectionStateChanged() {
result_view_->OnSelectionStateChanged();
if (header_view_ && header_view_->GetVisible())
header_view_->UpdateUI();
}
views::View* OmniboxRowView::GetActiveAuxiliaryButtonForAccessibility() const {
DCHECK(model_->GetPopupSelection().IsButtonFocused());
if (model_->GetPopupSelection().state ==
OmniboxPopupSelection::FOCUSED_BUTTON_HEADER) {
return header_view_->header_toggle_button();
}
return result_view_->GetActiveAuxiliaryButtonForAccessibility();
}
gfx::Insets OmniboxRowView::GetInsets() const {
// A visible header means this is the start of a new section. Give the section
// that just ended an extra 4dp of padding. https://crbug.com/1076646
if (line_ != 0 && header_view_ && header_view_->GetVisible())
return gfx::Insets::TLBR(4, 0, 0, 0);
return gfx::Insets();
}
BEGIN_METADATA(OmniboxRowView, views::View)
END_METADATA