| // 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(); |
| } |