blob: da79a97d72ec8ee3cd3cf8489ed01455c15e105a [file] [log] [blame]
// Copyright (c) 2012 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.
// For WinDDK ATL compatibility, these ATL headers must come first.
#include "build/build_config.h"
#if defined(OS_WIN)
#include <atlbase.h> // NOLINT
#include <atlwin.h> // NOLINT
#endif
#include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
#include <limits.h>
#include <algorithm> // NOLINT
#include "base/feature_list.h"
#include "base/i18n/bidi_line_iterator.h"
#include "base/macros.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/omnibox/omnibox_theme.h"
#include "chrome/browser/ui/views/location_bar/background_with_1_px_border.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_match_cell_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_tab_switch_button.h"
#include "chrome/browser/ui/views/omnibox/omnibox_text_view.h"
#include "chrome/browser/ui/views/omnibox/rounded_omnibox_results_frame.h"
#include "chrome/grit/generated_resources.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_popup_model.h"
#include "components/omnibox/browser/vector_icons.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/theme_provider.h"
#include "ui/events/event.h"
#include "ui/gfx/paint_vector_icon.h"
namespace {
// Creates a views::Background for the current result style.
std::unique_ptr<views::Background> CreateBackgroundWithColor(SkColor bg_color) {
return ui::MaterialDesignController::IsNewerMaterialUi()
? views::CreateSolidBackground(bg_color)
: std::make_unique<BackgroundWith1PxBorder>(bg_color, bg_color);
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, public:
OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model,
int model_index)
: model_(model),
model_index_(model_index),
is_hovered_(false),
animation_(new gfx::SlideAnimation(this)) {
CHECK_GE(model_index, 0);
AddChildView(suggestion_view_ = new OmniboxMatchCellView(this));
AddChildView(keyword_view_ = new OmniboxMatchCellView(this));
keyword_view_->icon()->EnableCanvasFlippingForRTLUI(true);
keyword_view_->icon()->SetImage(gfx::CreateVectorIcon(
omnibox::kKeywordSearchIcon, GetLayoutConstant(LOCATION_BAR_ICON_SIZE),
GetColor(OmniboxPart::RESULTS_ICON)));
keyword_view_->icon()->SizeToPreferredSize();
}
OmniboxResultView::~OmniboxResultView() {}
SkColor OmniboxResultView::GetColor(OmniboxPart part) const {
return GetOmniboxColor(part, GetTint(), GetThemeState());
}
void OmniboxResultView::SetMatch(const AutocompleteMatch& match) {
match_ = match.GetMatchWithContentsAndDescriptionPossiblySwapped();
animation_->Reset();
is_hovered_ = false;
suggestion_view_->OnMatchUpdate(this, match_);
keyword_view_->OnMatchUpdate(this, match_);
// Set up 'switch to tab' button.
if (match.has_tab_match && !match_.associated_keyword.get()) {
suggestion_tab_switch_button_ =
std::make_unique<OmniboxTabSwitchButton>(model_, this);
suggestion_tab_switch_button_->set_owned_by_client();
AddChildView(suggestion_tab_switch_button_.get());
} else {
suggestion_tab_switch_button_.reset();
}
Invalidate();
Layout();
}
void OmniboxResultView::ShowKeyword(bool show_keyword) {
if (show_keyword)
animation_->Show();
else
animation_->Hide();
}
void OmniboxResultView::Invalidate() {
// TODO(tapted): Consider using background()->SetNativeControlColor() and
// always have a background.
if (GetThemeState() == OmniboxPartState::NORMAL) {
SetBackground(nullptr);
} else {
SkColor color = GetColor(OmniboxPart::RESULTS_BACKGROUND);
SetBackground(CreateBackgroundWithColor(color));
}
// Reapply the dim color to account for the highlight state.
const OmniboxPart dim = OmniboxPart::RESULTS_TEXT_DIMMED;
suggestion_view_->separator()->ApplyTextColor(dim);
keyword_view_->separator()->ApplyTextColor(dim);
if (suggestion_tab_switch_button_)
suggestion_tab_switch_button_->UpdateBackground();
// 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.
// TODO(dschuyler): determine whether to optimize the color changes.
suggestion_view_->icon()->SetImage(GetIcon().ToImageSkia());
keyword_view_->icon()->SetImage(gfx::CreateVectorIcon(
omnibox::kKeywordSearchIcon, GetLayoutConstant(LOCATION_BAR_ICON_SIZE),
GetColor(OmniboxPart::RESULTS_ICON)));
// Answers use their own styling for additional content text and the
// description text, whereas non-answer suggestions use the match text and
// calculated classifications for the description text.
if (match_.answer) {
if (OmniboxFieldTrial::IsReverseAnswersEnabled()) {
// Answers may swap the content and description fields to change emphasis.
// But even when fields swap, the font size and color changes should not.
OmniboxTextView* primary = suggestion_view_->content();
OmniboxTextView* secondary = suggestion_view_->description();
bool swap = !match_.IsExceptedFromLineReversal();
if (swap)
std::swap(primary, secondary);
primary->SetText(match_.contents, match_.contents_class, swap ? -1 : 0);
primary->AppendExtraText(match_.answer->first_line());
primary->ApplyTextColor(swap ? dim : OmniboxPart::RESULTS_TEXT_DEFAULT);
secondary->SetText(match_.answer->second_line(), swap ? 0 : -1);
secondary->ApplyTextColor(swap ? OmniboxPart::RESULTS_TEXT_DEFAULT : dim);
} else {
suggestion_view_->content()->SetText(match_.contents,
match_.contents_class);
suggestion_view_->content()->AppendExtraText(match_.answer->first_line());
suggestion_view_->description()->SetText(match_.answer->second_line());
}
} else {
// Content and description use match text and calculated classifications.
suggestion_view_->content()->SetText(match_.contents,
match_.contents_class);
suggestion_view_->description()->SetText(match_.description,
match_.description_class);
}
AutocompleteMatch* keyword_match = match_.associated_keyword.get();
// Setting the keyword_view_ invisible is a minor optimization (it avoids
// some OnPaint calls); it is not required.
keyword_view_->SetVisible(keyword_match);
if (keyword_match) {
keyword_view_->content()->SetText(keyword_match->contents,
keyword_match->contents_class);
keyword_view_->description()->SetText(keyword_match->description,
keyword_match->description_class);
keyword_view_->description()->ApplyTextColor(dim);
}
}
void OmniboxResultView::OnSelected() {
DCHECK(IsSelected());
// 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.
NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
}
OmniboxPartState OmniboxResultView::GetThemeState() const {
if (IsSelected()) {
return is_hovered_ ? OmniboxPartState::HOVERED_AND_SELECTED
: OmniboxPartState::SELECTED;
}
return is_hovered_ ? OmniboxPartState::HOVERED : OmniboxPartState::NORMAL;
}
OmniboxTint OmniboxResultView::GetTint() const {
return model_->GetTint();
}
void OmniboxResultView::OnMatchIconUpdated() {
// The new icon will be fetched during Invalidate().
Invalidate();
SchedulePaint();
}
void OmniboxResultView::SetRichSuggestionImage(const gfx::ImageSkia& image) {
suggestion_view_->image()->SetImage(image);
Layout();
SchedulePaint();
}
////////////////////////////////////////////////////////////////////////////////
// views::ButtonListener overrides:
// |button| is the tab switch button.
void OmniboxResultView::ButtonPressed(views::Button* button,
const ui::Event& event) {
OpenMatch(WindowOpenDisposition::SWITCH_TO_TAB);
}
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, views::View overrides:
void OmniboxResultView::Layout() {
views::View::Layout();
// NOTE: While animating the keyword match, both matches may be visible.
int suggestion_width = width();
AutocompleteMatch* keyword_match = match_.associated_keyword.get();
if (keyword_match) {
const int max_kw_x =
suggestion_width - keyword_view_->IconWidthAndPadding();
suggestion_width = animation_->CurrentValueBetween(max_kw_x, 0);
}
if (suggestion_tab_switch_button_) {
suggestion_tab_switch_button_->ProvideWidthHint(suggestion_width);
const gfx::Size ts_button_size =
suggestion_tab_switch_button_->GetPreferredSize();
if (ts_button_size.width() > 0) {
suggestion_tab_switch_button_->SetSize(ts_button_size);
// It looks nice to have the same margin on top, bottom and right side.
const int margin =
(suggestion_view_->height() - ts_button_size.height()) / 2;
suggestion_width -= ts_button_size.width() + margin;
suggestion_tab_switch_button_->SetPosition(
gfx::Point(suggestion_width, margin));
suggestion_tab_switch_button_->SetVisible(true);
} else {
suggestion_tab_switch_button_->SetVisible(false);
}
}
keyword_view_->SetBounds(suggestion_width, 0, width(), height());
suggestion_view_->SetBounds(0, 0, suggestion_width, height());
}
bool OmniboxResultView::OnMousePressed(const ui::MouseEvent& event) {
if (event.IsOnlyLeftMouseButton())
model_->SetSelectedLine(model_index_);
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 (!IsSelected())
model_->SetSelectedLine(model_index_);
if (suggestion_tab_switch_button_) {
gfx::Point point_in_child_coords(event.location());
View::ConvertPointToTarget(this, suggestion_tab_switch_button_.get(),
&point_in_child_coords);
if (suggestion_tab_switch_button_->HitTestPoint(
point_in_child_coords)) {
SetMouseHandler(suggestion_tab_switch_button_.get());
return false;
}
}
} else {
SetHovered(true);
}
return true;
}
// When the drag leaves the bounds of this view, cancel the hover state and
// pass control to the popup view.
SetHovered(false);
SetMouseHandler(model_);
return false;
}
void OmniboxResultView::OnMouseReleased(const ui::MouseEvent& event) {
if (event.IsOnlyMiddleMouseButton() || event.IsOnlyLeftMouseButton()) {
OpenMatch(event.IsOnlyLeftMouseButton()
? WindowOpenDisposition::CURRENT_TAB
: WindowOpenDisposition::NEW_BACKGROUND_TAB);
}
}
void OmniboxResultView::OnMouseMoved(const ui::MouseEvent& event) {
SetHovered(true);
}
void OmniboxResultView::OnMouseExited(const ui::MouseEvent& event) {
SetHovered(false);
}
void OmniboxResultView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
// 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.
node_data->SetName(
AutocompleteMatchType::ToAccessibilityLabel(match_, match_.contents));
node_data->role = ax::mojom::Role::kListBoxOption;
node_data->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet,
model_index_ + 1);
node_data->AddIntAttribute(ax::mojom::IntAttribute::kSetSize,
model_->child_count());
node_data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected,
IsSelected());
if (is_hovered_)
node_data->AddState(ax::mojom::State::kHovered);
}
gfx::Size OmniboxResultView::CalculatePreferredSize() const {
// The keyword_view_ is not added because keyword_view_ uses the same space as
// suggestion_view_. So the 'preferred' size is just the suggestion_view_
// size.
return suggestion_view_->CalculatePreferredSize();
}
void OmniboxResultView::OnNativeThemeChanged(const ui::NativeTheme* theme) {
Invalidate();
SchedulePaint();
}
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, private:
gfx::Image OmniboxResultView::GetIcon() const {
return model_->GetMatchIcon(match_, GetColor(OmniboxPart::RESULTS_ICON));
}
void OmniboxResultView::SetHovered(bool hovered) {
if (is_hovered_ != hovered) {
is_hovered_ = hovered;
Invalidate();
SchedulePaint();
}
}
bool OmniboxResultView::IsSelected() const {
return model_->IsSelectedIndex(model_index_);
}
void OmniboxResultView::OpenMatch(WindowOpenDisposition disposition) {
model_->OpenMatch(model_index_, disposition);
}
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, views::View overrides, private:
const char* OmniboxResultView::GetClassName() const {
return "OmniboxResultView";
}
void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
animation_->SetSlideDuration(width() / 4);
Layout();
}
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, gfx::AnimationProgressed overrides, private:
void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) {
Layout();
SchedulePaint();
}