blob: be2b23f866754c273cafd7f1ff5d90c5c4e972aa [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_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/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/range/range.h"
#include "ui/gfx/render_text.h"
#include "ui/gfx/text_utils.h"
#include "ui/native_theme/native_theme.h"
using ui::NativeTheme;
namespace {
// The vertical margin that should be used above and below each suggestion.
static const int kVerticalMargin = 1;
// The vertical padding to provide each RenderText in addition to the height of
// the font. Where possible, RenderText uses this additional space to vertically
// center the cap height of the font instead of centering the entire font.
static const int kVerticalPadding = 4;
// TODO(dschuyler): Perhaps this should be based on the font size
// instead of hardcoded to 2 dp (e.g. by adding a space in an
// appropriate font to the beginning of the description, then reducing
// the additional padding here to zero).
static const int kAnswerIconToTextPadding = 2;
// A mapping from OmniboxResultView's ResultViewState/ColorKind types to
// NativeTheme colors.
struct TranslationTable {
ui::NativeTheme::ColorId id;
OmniboxResultView::ResultViewState state;
OmniboxResultView::ColorKind kind;
} static const kTranslationTable[] = {
{ NativeTheme::kColorId_ResultsTableNormalText,
OmniboxResultView::NORMAL, OmniboxResultView::TEXT },
{ NativeTheme::kColorId_ResultsTableHoveredText,
OmniboxResultView::HOVERED, OmniboxResultView::TEXT },
{ NativeTheme::kColorId_ResultsTableSelectedText,
OmniboxResultView::SELECTED, OmniboxResultView::TEXT },
{ NativeTheme::kColorId_ResultsTableNormalDimmedText,
OmniboxResultView::NORMAL, OmniboxResultView::DIMMED_TEXT },
{ NativeTheme::kColorId_ResultsTableHoveredDimmedText,
OmniboxResultView::HOVERED, OmniboxResultView::DIMMED_TEXT },
{ NativeTheme::kColorId_ResultsTableSelectedDimmedText,
OmniboxResultView::SELECTED, OmniboxResultView::DIMMED_TEXT },
{ NativeTheme::kColorId_ResultsTableNormalUrl,
OmniboxResultView::NORMAL, OmniboxResultView::URL },
{ NativeTheme::kColorId_ResultsTableHoveredUrl,
OmniboxResultView::HOVERED, OmniboxResultView::URL },
{ NativeTheme::kColorId_ResultsTableSelectedUrl,
OmniboxResultView::SELECTED, OmniboxResultView::URL },
};
// Whether to use the two-line layout.
bool IsTwoLineLayout() {
return base::FeatureList::IsEnabled(omnibox::kUIExperimentVerticalLayout) ||
ui::MaterialDesignController::IsTouchOptimizedUiEnabled();
}
// Creates a views::Background for the current result style.
std::unique_ptr<views::Background> CreateBackgroundWithColor(SkColor bg_color) {
return ui::MaterialDesignController::IsTouchOptimizedUiEnabled()
? views::CreateSolidBackground(bg_color)
: std::make_unique<BackgroundWith1PxBorder>(bg_color, bg_color);
}
// Returns the horizontal offset that ensures icons align vertically with the
// Omnibox icon.
int GetIconAlignmentOffset() {
// The horizontal bounds of a result is the width of the selection highlight
// (i.e. the views::Background). The traditional popup is designed with its
// selection shape mimicking the internal shape of the omnibox border. Inset
// to be consistent with the border drawn in BackgroundWith1PxBorder.
int offset = BackgroundWith1PxBorder::kLocationBarBorderThicknessDip;
// The touch-optimized popup selection always fills the results frame. So to
// align icons, inset additionally by the frame alignment inset on the left.
if (ui::MaterialDesignController::IsTouchOptimizedUiEnabled())
offset += RoundedOmniboxResultsFrame::kLocationBarAlignmentInsets.left();
return offset;
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, public:
OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model,
int model_index,
const gfx::FontList& font_list)
: model_(model),
model_index_(model_index),
is_hovered_(false),
font_height_(std::max(
font_list.GetHeight(),
font_list.DeriveWithWeight(gfx::Font::Weight::BOLD).GetHeight())),
animation_(new gfx::SlideAnimation(this)),
icon_view_(AddImageView()),
image_view_(AddImageView()),
keyword_icon_view_(AddImageView()),
content_view_(AddOmniboxTextView(font_list)),
description_view_(AddOmniboxTextView(font_list)),
keyword_content_view_(AddOmniboxTextView(font_list)),
keyword_description_view_(AddOmniboxTextView(font_list)),
separator_view_(AddOmniboxTextView(font_list)) {
CHECK_GE(model_index, 0);
keyword_icon_view_->EnableCanvasFlippingForRTLUI(true);
keyword_icon_view_->SetImage(gfx::CreateVectorIcon(
omnibox::kKeywordSearchIcon, GetLayoutConstant(LOCATION_BAR_ICON_SIZE),
GetVectorIconColor()));
keyword_icon_view_->SizeToPreferredSize();
tab_switch_button_ = new OmniboxTabSwitchButton(this);
AddChildView(tab_switch_button_);
tab_switch_button_->SetVisible(false);
}
OmniboxResultView::~OmniboxResultView() {}
SkColor OmniboxResultView::GetColor(
ResultViewState state,
ColorKind kind) const {
if (kind == INVISIBLE_TEXT)
return SK_ColorTRANSPARENT;
for (size_t i = 0; i < arraysize(kTranslationTable); ++i) {
if (kTranslationTable[i].state == state &&
kTranslationTable[i].kind == kind) {
return GetNativeTheme()->GetSystemColor(kTranslationTable[i].id);
}
}
NOTREACHED();
return gfx::kPlaceholderColor;
}
void OmniboxResultView::SetMatch(const AutocompleteMatch& match) {
match_ = match.GetMatchWithContentsAndDescriptionPossiblySwapped();
animation_->Reset();
is_hovered_ = false;
icon_view_->SetImage(GetIcon().ToImageSkia());
image_view_->SetVisible(false); // Until SetAnswerImage is called.
keyword_icon_view_->SetVisible(match_.associated_keyword.get());
if (OmniboxFieldTrial::InTabSwitchSuggestionWithButtonTrial()) {
tab_switch_button_->SetVisible(match.type ==
AutocompleteMatchType::TAB_SEARCH);
}
Invalidate();
if (GetWidget())
Layout();
}
void OmniboxResultView::ShowKeyword(bool show_keyword) {
if (show_keyword)
animation_->Show();
else
animation_->Hide();
}
void OmniboxResultView::Invalidate() {
const ResultViewState state = GetState();
if (state == NORMAL) {
SetBackground(nullptr);
} else {
SkColor color = GetOmniboxColor(OmniboxPart::RESULTS_BACKGROUND, GetTint(),
GetThemeState());
SetBackground(CreateBackgroundWithColor(color));
}
if (match_.answer) {
content_view_->SetText(match_.answer->first_line());
description_view_->SetText(match_.answer->second_line());
} else {
content_view_->SetText(match_.contents, match_.contents_class);
description_view_->SetText(match_.description, match_.description_class);
}
const base::string16& separator =
l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR);
separator_view_->SetText(separator);
separator_view_->Dim();
AutocompleteMatch* keyword_match = match_.associated_keyword.get();
keyword_content_view_->SetVisible(keyword_match);
keyword_description_view_->SetVisible(keyword_match);
if (keyword_match) {
keyword_content_view_->SetText(keyword_match->contents,
keyword_match->contents_class);
keyword_description_view_->SetText(keyword_match->description,
keyword_match->description_class);
keyword_description_view_->Dim();
}
// TODO(dschuyler): without this Layout call the text will shift slightly when
// hovered. Look into removing this call (without the text shifting).
Layout();
}
void OmniboxResultView::OnSelected() {
DCHECK_EQ(SELECTED, GetState());
// 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);
}
OmniboxResultView::ResultViewState OmniboxResultView::GetState() const {
if (model_->IsSelectedIndex(model_index_))
return SELECTED;
return is_hovered_ ? HOVERED : NORMAL;
}
OmniboxPartState OmniboxResultView::GetThemeState() const {
if (model_->IsSelectedIndex(model_index_)) {
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 repaint.
SchedulePaint();
}
void OmniboxResultView::SetAnswerImage(const gfx::ImageSkia& image) {
image_view_->SetImage(image);
image_view_->SetVisible(true);
Layout();
SchedulePaint();
}
void OmniboxResultView::OpenMatch(WindowOpenDisposition disposition) {
model_->OpenMatch(model_index_, disposition);
}
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, views::View overrides:
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 (GetState() != SELECTED)
model_->SetSelectedLine(model_index_);
if (tab_switch_button_->visible()) {
gfx::Point point_in_child_coords(event.location());
View::ConvertPointToTarget(this, tab_switch_button_,
&point_in_child_coords);
if (tab_switch_button_->HitTestPoint(point_in_child_coords)) {
SetMouseHandler(tab_switch_button_);
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->AddState(ax::mojom::State::kSelectable);
switch (GetState()) {
case SELECTED:
node_data->AddState(ax::mojom::State::kSelected);
break;
case HOVERED:
node_data->AddState(ax::mojom::State::kHovered);
break;
default:
break;
}
}
gfx::Size OmniboxResultView::CalculatePreferredSize() const {
int height = GetTextHeight() + (2 * GetVerticalMargin());
if (match_.answer)
height += GetAnswerHeight();
else if (IsTwoLineLayout())
height += GetTextHeight();
return gfx::Size(0, height);
}
void OmniboxResultView::OnNativeThemeChanged(const ui::NativeTheme* theme) {
Invalidate();
SchedulePaint();
}
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, private:
views::ImageView* OmniboxResultView::AddImageView() {
views::ImageView* view = new views::ImageView();
AddChildView(view);
return view;
}
OmniboxTextView* OmniboxResultView::AddOmniboxTextView(
const gfx::FontList& font_list) {
OmniboxTextView* view = new OmniboxTextView(this, font_list);
AddChildView(view);
return view;
}
int OmniboxResultView::GetTextHeight() const {
return font_height_ + kVerticalPadding;
}
gfx::Image OmniboxResultView::GetIcon() const {
return model_->GetMatchIcon(match_, GetVectorIconColor());
}
SkColor OmniboxResultView::GetVectorIconColor() const {
// For selected rows, paint the icon the same color as the text.
SkColor color = GetColor(GetState(), TEXT);
if (GetState() != SELECTED)
color = color_utils::DeriveDefaultIconColor(color);
return color;
}
bool OmniboxResultView::ShowOnlyKeywordMatch() const {
return match_.associated_keyword &&
(keyword_icon_view_->x() <= (icon_view_->x() + icon_view_->width()));
}
int OmniboxResultView::GetAnswerHeight() const {
const int horizontal_padding =
GetLayoutConstant(LOCATION_BAR_PADDING) +
GetLayoutConstant(LOCATION_BAR_ICON_INTERIOR_PADDING);
const gfx::Image icon = GetIcon();
int icon_width = icon.Width();
int answer_icon_size = image_view_->visible()
? image_view_->height() + kAnswerIconToTextPadding
: 0;
// TODO(dschuyler): The GetIconAlignmentOffset() is applied an extra time to
// match the math in Layout(). This seems like a (minor) mistake.
int deduction = (GetIconAlignmentOffset() * 2) + icon_width +
(horizontal_padding * 3) + answer_icon_size;
int description_width = std::max(width() - deduction, 0);
return description_view_->GetHeightForWidth(description_width) +
kVerticalPadding;
}
int OmniboxResultView::GetVerticalMargin() const {
// Regardless of the text size, we ensure a minimum size for the content line
// here. This minimum is larger for hybrid mouse/touch devices to ensure an
// adequately sized touch target.
using Md = ui::MaterialDesignController;
const int kIconVerticalPad = base::GetFieldTrialParamByFeatureAsInt(
omnibox::kUIExperimentVerticalMargin,
OmniboxFieldTrial::kUIVerticalMarginParam,
Md::GetMode() == Md::MATERIAL_HYBRID ? 8 : 4);
const int min_height =
GetLayoutConstant(LOCATION_BAR_ICON_SIZE) + (kIconVerticalPad * 2);
return std::max(kVerticalMargin, (min_height - GetTextHeight()) / 2);
}
void OmniboxResultView::SetHovered(bool hovered) {
if (is_hovered_ != hovered) {
is_hovered_ = hovered;
Invalidate();
SchedulePaint();
}
}
////////////////////////////////////////////////////////////////////////////////
// OmniboxResultView, views::View overrides, private:
void OmniboxResultView::Layout() {
views::View::Layout();
const int horizontal_padding =
GetLayoutConstant(LOCATION_BAR_PADDING) +
GetLayoutConstant(LOCATION_BAR_ICON_INTERIOR_PADDING);
const int start_x = GetIconAlignmentOffset() + horizontal_padding;
int end_x = width() - start_x;
int text_height = GetTextHeight();
int row_height = text_height;
if (IsTwoLineLayout())
row_height += match_.answer ? GetAnswerHeight() : GetTextHeight();
const gfx::Image icon = GetIcon();
const int icon_y = GetVerticalMargin() + (row_height - icon.Height()) / 2;
icon_view_->SetBounds(start_x, icon_y, icon.Width(), icon.Height());
icon_view_->SetVisible(!ShowOnlyKeywordMatch());
separator_view_->SetVisible(false);
// TODO(dschuyler): Refactor these if/else's into separate pieces of code to
// improve readability/maintainability.
AutocompleteMatch* keyword_match = match_.associated_keyword.get();
if (keyword_match) {
// NOTE: While animating the keyword match, both matches may be visible.
const int max_kw_x = end_x - keyword_icon_view_->width();
int kw_x = animation_->CurrentValueBetween(max_kw_x, start_x);
end_x = kw_x;
int y = GetVerticalMargin();
kw_x += BackgroundWith1PxBorder::kLocationBarBorderThicknessDip;
keyword_icon_view_->SetPosition(
gfx::Point(kw_x, (height() - keyword_icon_view_->height()) / 2));
kw_x += keyword_icon_view_->width() + horizontal_padding;
keyword_content_view_->SizeToPreferredSize();
int first_width = keyword_content_view_->GetContentsBounds().width();
keyword_description_view_->SizeToPreferredSize();
int second_width =
keyword_description_view_
? keyword_description_view_->GetContentsBounds().width()
: 0;
OmniboxPopupModel::ComputeMatchMaxWidths(
first_width, separator_view_->width(), second_width, width(),
/*description_on_separate_line=*/false,
!AutocompleteMatch::IsSearchType(match_.type), &first_width,
&second_width);
keyword_content_view_->SetBounds(kw_x, y, first_width, text_height);
if (second_width != 0) {
kw_x += keyword_content_view_->width();
separator_view_->SetVisible(true);
separator_view_->SetBounds(kw_x, y, separator_view_->width(),
text_height);
kw_x += separator_view_->width();
keyword_description_view_->SetBounds(kw_x, y, second_width, text_height);
} else if (IsTwoLineLayout()) {
keyword_content_view_->SetSize(gfx::Size(first_width, text_height * 2));
}
}
if (OmniboxFieldTrial::InTabSwitchSuggestionWithButtonTrial() &&
match_.type == AutocompleteMatchType::TAB_SEARCH) {
const int ts_button_width = tab_switch_button_->GetPreferredSize().width();
const int ts_button_height = height();
tab_switch_button_->SetSize(gfx::Size(ts_button_width, ts_button_height));
const int ts_x = end_x - ts_button_width + horizontal_padding;
end_x = ts_x - start_x - horizontal_padding;
tab_switch_button_->SetPosition(gfx::Point(ts_x, 0));
}
// NOTE: While animating the keyword match, both matches may be visible.
if (!ShowOnlyKeywordMatch()) {
description_view_->SizeToPreferredSize();
int x = start_x;
x += icon.Width() + horizontal_padding;
int y = GetVerticalMargin();
if (match_.answer) {
content_view_->SetBounds(x, y, end_x - x, text_height);
y += text_height;
if (image_view_->visible()) {
// The description may be multi-line. Using the view height results in
// an image that's too large, so we use the line height here instead.
int image_edge_length = description_view_->GetLineHeight();
image_view_->SetBounds(
start_x + icon_view_->width() + horizontal_padding,
y + (kVerticalPadding / 2), image_edge_length, image_edge_length);
image_view_->SetImageSize(
gfx::Size(image_edge_length, image_edge_length));
x += image_view_->width() + kAnswerIconToTextPadding;
}
int description_width = end_x - x;
description_view_->SetBounds(
x, y, description_width,
description_view_->GetHeightForWidth(description_width) +
kVerticalPadding);
} else if (IsTwoLineLayout()) {
if (!!description_view_->GetContentsBounds().width()) {
// A description is present.
content_view_->SetBounds(x, y, end_x - x, GetTextHeight());
y += GetTextHeight();
int description_width = end_x - x;
description_view_->SetBounds(
x, y, description_width,
description_view_->GetHeightForWidth(description_width) +
kVerticalPadding);
} else {
// For no description, shift down halfway to draw contents in middle.
y += GetTextHeight() / 2;
content_view_->SetBounds(x, y, end_x - x, GetTextHeight());
}
} else {
content_view_->SizeToPreferredSize();
int first_width = content_view_->GetContentsBounds().width();
int second_width = description_view_
? description_view_->GetContentsBounds().width()
: 0;
OmniboxPopupModel::ComputeMatchMaxWidths(
first_width, separator_view_->width(), second_width, end_x - x,
/*description_on_separate_line=*/false,
!AutocompleteMatch::IsSearchType(match_.type), &first_width,
&second_width);
OmniboxTextView* first_view = content_view_;
OmniboxTextView* second_view = description_view_;
first_view->SetBounds(x, y, first_width, text_height);
x += first_width;
if (second_width) {
separator_view_->SetVisible(true);
separator_view_->SetBounds(x, y, separator_view_->width(), text_height);
x += separator_view_->width();
}
second_view->SetBounds(x, y, second_width, text_height);
}
}
}
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();
}