blob: c96ef7cc293580c629289fabc95982b9afcd7d63 [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.
#include "ash/app_list/views/search_box_view.h"
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/model/search/search_box_model.h"
#include "ash/app_list/model/search/search_model.h"
#include "ash/app_list/resources/grit/app_list_resources.h"
#include "ash/app_list/views/app_list_view.h"
#include "ash/app_list/views/contents_view.h"
#include "ash/app_list/views/expand_arrow_view.h"
#include "ash/app_list/views/result_selection_controller.h"
#include "ash/app_list/views/search_result_base_view.h"
#include "ash/app_list/views/search_result_page_view.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/app_list/app_list_color_provider.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/vector_icons/vector_icons.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/wallpaper_types.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/search_box/search_box_constants.h"
#include "ash/search_box/search_box_view_delegate.h"
#include "base/bind.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "chromeos/constants/chromeos_switches.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/ime/composition_text.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/border.h"
#include "ui/views/context_menu_controller.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
constexpr int kSearchBoxFocusRingWidth = 2;
// Padding between the focus ring and the search box view
constexpr int kSearchBoxFocusRingPadding = 4;
constexpr int kSearchBoxFocusRingCornerRadius = 28;
// Minimum amount of characters required to enable autocomplete.
constexpr int kMinimumLengthToAutocomplete = 2;
float GetAssistantButtonOpacityForState(AppListState state) {
if (state == AppListState::kStateSearchResults)
return .0f;
return 1.f;
}
bool IsTrimmedQueryEmpty(const base::string16& query) {
base::string16 trimmed_query;
base::TrimWhitespace(query, base::TrimPositions::TRIM_ALL, &trimmed_query);
return trimmed_query.empty();
}
} // namespace
SearchBoxView::SearchBoxView(SearchBoxViewDelegate* delegate,
AppListViewDelegate* view_delegate,
AppListView* app_list_view)
: SearchBoxViewBase(delegate),
view_delegate_(view_delegate),
app_list_view_(app_list_view),
is_app_list_search_autocomplete_enabled_(
app_list_features::IsAppListSearchAutocompleteEnabled()) {}
SearchBoxView::~SearchBoxView() {
search_model_->search_box()->RemoveObserver(this);
}
void SearchBoxView::Init(bool is_tablet_mode) {
is_tablet_mode_ = is_tablet_mode;
set_show_close_button_when_active(true);
SearchBoxViewBase::Init();
UpdatePlaceholderTextAndAccessibleName();
current_query_ = search_box()->GetText();
}
void SearchBoxView::OnTabletModeChanged(bool started) {
is_tablet_mode_ = started;
UpdateKeyboardVisibility();
// Search box accessible name may change depending on tablet mode state.
UpdatePlaceholderTextAndAccessibleName();
UpdateSearchBoxBorder();
}
void SearchBoxView::ResetForShow() {
// Avoid clearing an already inactive SearchBox in tablet mode because this
// causes suggested chips to flash (http://crbug.com/979594).
if (!is_search_box_active() && is_tablet_mode_)
return;
ClearSearchAndDeactivateSearchBox();
SetSearchBoxBackgroundCornerRadius(
GetSearchBoxBorderCornerRadiusForState(contents_view_->GetActiveState()));
}
void SearchBoxView::ClearSearch() {
SearchBoxViewBase::ClearSearch();
current_query_.clear();
app_list_view_->SetStateFromSearchBoxView(
true, false /*triggered_by_contents_change*/);
}
void SearchBoxView::HandleSearchBoxEvent(ui::LocatedEvent* located_event) {
if (located_event->type() == ui::ET_MOUSEWHEEL) {
if (!app_list_view_->HandleScroll(
located_event->location(),
located_event->AsMouseWheelEvent()->offset(), ui::ET_MOUSEWHEEL)) {
return;
}
}
SearchBoxViewBase::HandleSearchBoxEvent(located_event);
}
void SearchBoxView::ModelChanged() {
if (search_model_)
search_model_->search_box()->RemoveObserver(this);
search_model_ = view_delegate_->GetSearchModel();
DCHECK(search_model_);
UpdateSearchIcon();
search_model_->search_box()->AddObserver(this);
OnWallpaperColorsChanged();
ShowAssistantChanged();
}
void SearchBoxView::UpdateKeyboardVisibility() {
if (!keyboard::KeyboardUIController::HasInstance())
return;
auto* const keyboard_controller = keyboard::KeyboardUIController::Get();
bool should_show_keyboard =
is_search_box_active() && search_box()->HasFocus();
if (!keyboard_controller->IsEnabled() ||
should_show_keyboard == keyboard_controller->IsKeyboardVisible()) {
return;
}
if (should_show_keyboard) {
keyboard_controller->ShowKeyboard(false);
return;
}
keyboard_controller->HideKeyboardByUser();
}
void SearchBoxView::UpdateModel(bool initiated_by_user) {
// Temporarily remove from observer to ignore notifications caused by us.
search_model_->search_box()->RemoveObserver(this);
search_model_->search_box()->Update(search_box()->GetText(),
initiated_by_user);
search_model_->search_box()->AddObserver(this);
}
void SearchBoxView::UpdateSearchIcon() {
const gfx::VectorIcon& google_icon =
is_search_box_active() ? kGoogleColorIcon : kGoogleBlackIcon;
const gfx::VectorIcon& icon = search_model_->search_engine_is_google()
? google_icon
: kSearchEngineNotGoogleIcon;
SetSearchIconImage(
gfx::CreateVectorIcon(icon, kSearchBoxIconSize,
AppListColorProvider::Get()->GetSearchBoxIconColor(
SkColorSetARGB(0xDE, 0x00, 0x00, 0x00))));
}
void SearchBoxView::UpdateSearchBoxBorder() {
// Creates an empty border to create a region for the focus ring to appear.
SetBorder(views::CreateEmptyBorder(gfx::Insets(GetFocusRingSpacing())));
}
void SearchBoxView::OnPaintBackground(gfx::Canvas* canvas) {
// Paints the focus ring if the search box is focused.
if (search_box()->HasFocus() && !is_search_box_active() &&
view_delegate_->KeyboardTraversalEngaged()) {
gfx::Rect bounds = GetContentsBounds();
bounds.Inset(-kSearchBoxFocusRingPadding, -kSearchBoxFocusRingPadding);
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(AppListColorProvider::Get()->GetFocusRingColor());
flags.setStyle(cc::PaintFlags::Style::kStroke_Style);
flags.setStrokeWidth(kSearchBoxFocusRingWidth);
canvas->DrawRoundRect(bounds, kSearchBoxFocusRingCornerRadius, flags);
}
}
const char* SearchBoxView::GetClassName() const {
return "SearchBoxView";
}
void SearchBoxView::OnThemeChanged() {
views::View::OnThemeChanged();
SetupAssistantButton();
SetupCloseButton();
OnWallpaperColorsChanged();
}
// static
int SearchBoxView::GetFocusRingSpacing() {
return kSearchBoxFocusRingWidth + kSearchBoxFocusRingPadding;
}
void SearchBoxView::SetupCloseButton() {
views::ImageButton* close = close_button();
close->SetCallback(base::BindRepeating(
[](SearchBoxView* view) {
view->view_delegate_->LogSearchAbandonHistogram();
view->SetSearchBoxActive(false, ui::ET_UNKNOWN);
view->ClearSearch();
},
this));
close->SetImage(
views::ImageButton::STATE_NORMAL,
gfx::CreateVectorIcon(views::kIcCloseIcon, kSearchBoxIconSize,
AppListColorProvider::Get()->GetSearchBoxIconColor(
gfx::kGoogleGrey700)));
close->SetVisible(false);
base::string16 close_button_label(
l10n_util::GetStringUTF16(IDS_APP_LIST_CLEAR_SEARCHBOX));
close->SetAccessibleName(close_button_label);
close->SetTooltipText(close_button_label);
}
void SearchBoxView::SetupBackButton() {
views::ImageButton* back = back_button();
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
back->SetImage(views::ImageButton::STATE_NORMAL,
rb.GetImageSkiaNamed(IDR_APP_LIST_FOLDER_BACK_NORMAL));
back->SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
back->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
back->SetVisible(false);
base::string16 back_button_label(
l10n_util::GetStringUTF16(IDS_APP_LIST_BACK));
back->SetAccessibleName(back_button_label);
back->SetTooltipText(back_button_label);
}
void SearchBoxView::RecordSearchBoxActivationHistogram(
ui::EventType event_type) {
ActivationSource activation_type;
switch (event_type) {
case ui::ET_GESTURE_TAP:
activation_type = ActivationSource::kGestureTap;
break;
case ui::ET_MOUSE_PRESSED:
activation_type = ActivationSource::kMousePress;
break;
case ui::ET_KEY_PRESSED:
activation_type = ActivationSource::kKeyPress;
break;
default:
return;
}
UMA_HISTOGRAM_ENUMERATION("Apps.AppListSearchBoxActivated", activation_type);
if (is_tablet_mode_) {
UMA_HISTOGRAM_ENUMERATION("Apps.AppListSearchBoxActivated.TabletMode",
activation_type);
} else {
UMA_HISTOGRAM_ENUMERATION("Apps.AppListSearchBoxActivated.ClamshellMode",
activation_type);
}
}
void SearchBoxView::OnSearchBoxActiveChanged(bool active) {
if (active) {
search_box()->SetAccessibleName(base::string16());
} else {
search_box()->SetAccessibleName(l10n_util::GetStringUTF16(
is_tablet_mode_
? IDS_APP_LIST_SEARCH_BOX_ACCESSIBILITY_NAME_TABLET
: IDS_APP_LIST_SEARCH_BOX_ACCESSIBILITY_NAME_CLAMSHELL));
}
}
void SearchBoxView::OnKeyEvent(ui::KeyEvent* event) {
app_list_view_->RedirectKeyEventToSearchBox(event);
if (!IsUnhandledUpDownKeyEvent(*event))
return;
// Handles arrow key events from the search box while the search box is
// inactive. This covers both folder traversal and apps grid traversal. Search
// result traversal is handled in |HandleKeyEvent|
AppListPage* page =
contents_view_->GetPageView(contents_view_->GetActivePageIndex());
views::View* arrow_view = contents_view_->expand_arrow_view();
views::View* next_view = nullptr;
if (event->key_code() == ui::VKEY_UP) {
if (arrow_view && arrow_view->IsFocusable())
next_view = arrow_view;
else
next_view = page->GetLastFocusableView();
} else {
next_view = page->GetFirstFocusableView();
}
if (next_view)
next_view->RequestFocus();
event->SetHandled();
}
bool SearchBoxView::OnMouseWheel(const ui::MouseWheelEvent& event) {
if (contents_view_)
return contents_view_->OnMouseWheel(event);
return false;
}
void SearchBoxView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
if (HasAutocompleteText()) {
node_data->role = ax::mojom::Role::kTextField;
node_data->SetValue(l10n_util::GetStringFUTF16(
IDS_APP_LIST_SEARCH_BOX_AUTOCOMPLETE, search_box()->GetText()));
}
}
void SearchBoxView::UpdateBackground(double progress,
AppListState current_state,
AppListState target_state) {
SetSearchBoxBackgroundCornerRadius(gfx::Tween::LinearIntValueBetween(
progress, GetSearchBoxBorderCornerRadiusForState(current_state),
GetSearchBoxBorderCornerRadiusForState(target_state)));
const SkColor color = gfx::Tween::ColorValueBetween(
progress, GetBackgroundColorForState(current_state),
GetBackgroundColorForState(target_state));
UpdateBackgroundColor(color);
search_box()->SetTextColor(AppListColorProvider::Get()->GetSearchBoxTextColor(
kDeprecatedSearchBoxTextDefaultColor));
}
void SearchBoxView::UpdateLayout(double progress,
AppListState current_state,
int current_state_height,
AppListState target_state,
int target_state_height) {
// Horizontal margins are selected to match search box icon's vertical
// margins.
const int horizontal_spacing = gfx::Tween::LinearIntValueBetween(
progress, (current_state_height - kSearchBoxIconSize) / 2,
(target_state_height - kSearchBoxIconSize) / 2);
const int horizontal_right_padding =
horizontal_spacing - (kSearchBoxButtonSizeDip - kSearchBoxIconSize) / 2;
box_layout()->set_inside_border_insets(
gfx::Insets(0, horizontal_spacing, 0, horizontal_right_padding));
box_layout()->set_between_child_spacing(horizontal_spacing);
if (show_assistant_button()) {
assistant_button()->layer()->SetOpacity(gfx::Tween::LinearIntValueBetween(
progress, GetAssistantButtonOpacityForState(current_state),
GetAssistantButtonOpacityForState(target_state)));
}
InvalidateLayout();
}
int SearchBoxView::GetSearchBoxBorderCornerRadiusForState(
AppListState state) const {
if (state == AppListState::kStateSearchResults &&
!app_list_view_->is_in_drag()) {
return kSearchBoxBorderCornerRadiusSearchResult;
}
return kSearchBoxBorderCornerRadius;
}
SkColor SearchBoxView::GetBackgroundColorForState(AppListState state) const {
if (state == AppListState::kStateSearchResults)
return AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor();
return AppListColorProvider::Get()->GetSearchBoxBackgroundColor();
}
void SearchBoxView::ShowZeroStateSuggestions() {
base::RecordAction(
base::UserMetricsAction("AppList_ShowZeroStateSuggestions"));
base::string16 empty_query;
ContentsChanged(search_box(), empty_query);
}
void SearchBoxView::OnWallpaperColorsChanged() {
UpdateSearchIcon();
AppListColorProvider* app_list_color_provider = AppListColorProvider::Get();
search_box()->set_placeholder_text_color(
app_list_color_provider->GetSearchBoxTextColor(
kDeprecatedSearchBoxPlaceholderTextColor));
search_box()->SetTextColor(app_list_color_provider->GetSearchBoxTextColor(
kDeprecatedSearchBoxTextDefaultColor));
if (features::IsDarkLightModeEnabled()) {
UpdateBackgroundColor(
app_list_color_provider->GetSearchBoxBackgroundColor());
}
SchedulePaint();
}
void SearchBoxView::ProcessAutocomplete() {
if (!ShouldProcessAutocomplete())
return;
SearchResultBaseView* const first_result_view =
contents_view_->search_results_page_view()->first_result_view();
if (!first_result_view || !first_result_view->selected())
return;
SearchResult* const first_visible_result = first_result_view->result();
if (first_result_view->is_default_result() &&
current_query_ != search_box()->GetText()) {
// Search box text has been set to the previous selected result. Reset
// it back to the current query. This could happen due to the racing
// between results update and user press key to select a result.
// See crbug.com/1065454.
search_box()->SetText(current_query_);
}
// Current non-autocompleted text.
const base::string16& user_typed_text =
search_box()->GetText().substr(0, highlight_range_.start());
if (last_key_pressed_ == ui::VKEY_BACK ||
last_key_pressed_ == ui::VKEY_DELETE || IsArrowKey(last_key_pressed_) ||
!first_visible_result ||
user_typed_text.length() < kMinimumLengthToAutocomplete) {
// If the suggestion was rejected, no results exist, or current text
// is too short for a confident autocomplete suggestion.
return;
}
const base::string16& details = first_visible_result->details();
const base::string16& search_text = first_visible_result->title();
if (base::StartsWith(details, user_typed_text,
base::CompareCase::INSENSITIVE_ASCII)) {
// Current text in the search_box matches the first result's url.
SetAutocompleteText(details);
return;
}
if (base::StartsWith(search_text, user_typed_text,
base::CompareCase::INSENSITIVE_ASCII)) {
// Current text in the search_box matches the first result's search result
// text.
SetAutocompleteText(search_text);
return;
}
// Current text in the search_box does not match the first result's url or
// search result text.
ClearAutocompleteText();
}
void SearchBoxView::UpdatePlaceholderTextAndAccessibleName() {
search_box()->SetPlaceholderText(
l10n_util::GetStringUTF16(IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER));
search_box()->SetAccessibleName(l10n_util::GetStringUTF16(
is_tablet_mode_ ? IDS_APP_LIST_SEARCH_BOX_ACCESSIBILITY_NAME_TABLET
: IDS_APP_LIST_SEARCH_BOX_ACCESSIBILITY_NAME_CLAMSHELL));
}
void SearchBoxView::AcceptAutocompleteText() {
if (!ShouldProcessAutocomplete())
return;
// Do not trigger another search here in case the user is left clicking to
// select existing autocomplete text. (This also matches omnibox behavior.)
DCHECK(HasAutocompleteText());
search_box()->ClearSelection();
ResetHighlightRange();
}
bool SearchBoxView::HasAutocompleteText() {
// If the selected range is non-empty, it will either be suggested by
// autocomplete or selected by the user. If the recorded autocomplete
// |highlight_range_| matches the selection range, this text is suggested by
// autocomplete.
return search_box()->GetSelectedRange().EqualsIgnoringDirection(
highlight_range_) &&
highlight_range_.length() > 0;
}
void SearchBoxView::ClearAutocompleteText() {
if (!ShouldProcessAutocomplete())
return;
// Avoid triggering subsequent query by temporarily setting controller to
// nullptr.
search_box()->set_controller(nullptr);
// search_box()->ClearCompositionText() does not work here because
// SetAutocompleteText() calls SelectRange(), which comfirms the active
// composition text (so there is nothing to clear here). Set empty composition
// text to clear the selected range.
search_box()->SetCompositionText(ui::CompositionText());
search_box()->set_controller(this);
ResetHighlightRange();
}
void SearchBoxView::OnBeforeUserAction(views::Textfield* sender) {
if (a11y_selection_on_search_result_) {
a11y_selection_on_search_result_ = false;
search_box()->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
}
}
void SearchBoxView::ContentsChanged(views::Textfield* sender,
const base::string16& new_contents) {
if (IsTrimmedQueryEmpty(current_query_) && !IsSearchBoxTrimmedQueryEmpty()) {
// User enters a new search query. Record the action.
base::RecordAction(base::UserMetricsAction("AppList_SearchQueryStarted"));
}
current_query_ = new_contents;
// Update autocomplete text highlight range to track user typed text.
if (ShouldProcessAutocomplete())
ResetHighlightRange();
SearchBoxViewBase::ContentsChanged(sender, new_contents);
app_list_view_->SetStateFromSearchBoxView(
IsSearchBoxTrimmedQueryEmpty(), true /*triggered_by_contents_change*/);
}
void SearchBoxView::SetAutocompleteText(
const base::string16& autocomplete_text) {
if (!ShouldProcessAutocomplete())
return;
const base::string16& current_text = search_box()->GetText();
// Currrent text is a prefix of autocomplete text.
DCHECK(base::StartsWith(autocomplete_text, current_text,
base::CompareCase::INSENSITIVE_ASCII));
// Don't set autocomplete text if it's the same as current search box text.
if (autocomplete_text == current_text)
return;
const base::string16& highlighted_text =
autocomplete_text.substr(highlight_range_.start());
// Don't set autocomplete text if the highlighted text is the same as before.
if (highlighted_text == search_box()->GetSelectedText())
return;
highlight_range_.set_end(autocomplete_text.length());
ui::CompositionText composition_text;
composition_text.text = highlighted_text;
composition_text.selection = gfx::Range(0, highlighted_text.length());
// Avoid triggering subsequent query by temporarily setting controller to
// nullptr.
search_box()->set_controller(nullptr);
search_box()->SetCompositionText(composition_text);
search_box()->set_controller(this);
// The controller was null briefly, so it was unaware of a highlight change.
// As a result, we need to manually declare the range to allow for proper
// selection behavior.
search_box()->SetSelectedRange(highlight_range_);
// Send an event to alert ChromeVox that an autocomplete has occurred.
// The |kValueChanged| type lets ChromeVox know that it should scan
// |node_data| for "Value".
NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true);
}
void SearchBoxView::UpdateQuery(const base::string16& new_query) {
search_box()->SetText(new_query);
ContentsChanged(search_box(), new_query);
}
void SearchBoxView::ClearSearchAndDeactivateSearchBox() {
if (!is_search_box_active())
return;
view_delegate_->LogSearchAbandonHistogram();
contents_view_->search_results_page_view()
->result_selection_controller()
->ClearSelection();
a11y_selection_on_search_result_ = false;
ClearSearch();
SetSearchBoxActive(false, ui::ET_UNKNOWN);
}
bool SearchBoxView::HandleKeyEvent(views::Textfield* sender,
const ui::KeyEvent& key_event) {
if (key_event.type() == ui::ET_KEY_RELEASED)
return false;
// Events occurring over an inactive search box are handled elsewhere, with
// the exception of left/right arrow key events, and return.
if (!is_search_box_active()) {
if (key_event.key_code() == ui::VKEY_RETURN) {
SetSearchBoxActive(true, key_event.type());
return true;
}
if (IsUnhandledLeftRightKeyEvent(key_event)) {
return ProcessLeftRightKeyTraversalForTextfield(search_box(), key_event);
}
return false;
}
// Nothing to do if no results are available (the rest of the method handles
// result actions and result traversal). This might happen if zero state
// suggestions are not enabled, and search box textfield is empty.
if (!contents_view_->search_results_page_view()->first_result_view())
return false;
ResultSelectionController* selection_controller =
contents_view_->search_results_page_view()->result_selection_controller();
// When search box is active, the focus cycles between close button and the
// search_box - when close button is focused, traversal keys (arrows and
// tab) should move the focus to the search box, and reset the selection
// (which might have been cleared when focus moved to the close button).
if (!search_box()->HasFocus()) {
// Only handle result traversal keys.
if (!IsUnhandledArrowKeyEvent(key_event) &&
key_event.key_code() != ui::VKEY_TAB) {
return false;
}
search_box()->RequestFocus();
if (selection_controller->MoveSelection(key_event) ==
ResultSelectionController::MoveResult::kResultChanged) {
UpdateSearchBoxTextForSelectedResult(
selection_controller->selected_result()->result());
}
return true;
}
// Handle return - opens the selected result.
if (key_event.key_code() == ui::VKEY_RETURN) {
// Hitting Enter when focus is on search box opens the selected result.
ui::KeyEvent event(key_event);
SearchResultBaseView* selected_result =
selection_controller->selected_result();
if (selected_result && selected_result->result())
selected_result->OnKeyEvent(&event);
return true;
}
// Allows alt+back and alt+delete as a shortcut for the 'remove result'
// dialog
if (key_event.IsAltDown() &&
((key_event.key_code() == ui::VKEY_BROWSER_BACK) ||
(key_event.key_code() == ui::VKEY_DELETE))) {
ui::KeyEvent event(key_event);
SearchResultBaseView* selected_result =
selection_controller->selected_result();
if (selected_result && selected_result->result())
selected_result->OnKeyEvent(&event);
// Reset the selected result to the default result.
selection_controller->ResetSelection(nullptr, true /* default_selection */);
search_box()->SetText(base::string16());
return true;
}
// Do not handle keys intended for result selection traversal here - these
// should be handled elsewhere, for example by the search box text field.
// Keys used for result selection traversal:
// * TAB
// * up/down key
// * left/right, if the selected container is horizontal. For vertical
// containers, left and right key should be handled by the text field
// (to move cursor, and clear or accept autocomplete suggestion).
const bool result_selection_traversal_key_event =
key_event.key_code() == ui::VKEY_TAB ||
IsUnhandledUpDownKeyEvent(key_event) ||
(IsUnhandledLeftRightKeyEvent(key_event) &&
selection_controller->selected_location_details() &&
selection_controller->selected_location_details()
->container_is_horizontal);
if (!result_selection_traversal_key_event) {
// Record the |last_key_pressed_| for autocomplete.
if (!search_box()->GetText().empty() && ShouldProcessAutocomplete())
last_key_pressed_ = key_event.key_code();
return false;
}
// Clear non-auto-complete generated selection to prevent navigation keys from
// deleting selected text.
if (search_box()->HasSelection() && !HasAutocompleteText())
search_box()->ClearSelection();
ResultSelectionController::MoveResult move_result =
selection_controller->MoveSelection(key_event);
switch (move_result) {
case ResultSelectionController::MoveResult::kNone:
// If the |ResultSelectionController| decided not to change selection,
// return early, as what follows is actions for updating based on
// change.
break;
case ResultSelectionController::MoveResult::kSelectionCycleRejected:
// If move was about to cycle, clear the selection and move the focus to
// the next element in the SearchBoxView - close_button() (only
// close_button() and search_box() are expected to be in the focus cycle
// while the search box is active).
if (HasAutocompleteText())
ClearAutocompleteText();
selection_controller->ClearSelection();
DCHECK(close_button()->GetVisible());
close_button()->RequestFocus();
close_button()->NotifyAccessibilityEvent(ax::mojom::Event::kSelection,
true);
a11y_selection_on_search_result_ = false;
break;
case ResultSelectionController::MoveResult::kResultChanged:
UpdateSearchBoxTextForSelectedResult(
selection_controller->selected_result()->result());
break;
}
return true;
}
bool SearchBoxView::HandleMouseEvent(views::Textfield* sender,
const ui::MouseEvent& mouse_event) {
if (mouse_event.type() == ui::ET_MOUSEWHEEL) {
return app_list_view_->HandleScroll(
mouse_event.location(), (&mouse_event)->AsMouseWheelEvent()->offset(),
ui::ET_MOUSEWHEEL);
}
if (mouse_event.type() == ui::ET_MOUSE_PRESSED && HasAutocompleteText())
AcceptAutocompleteText();
// Don't activate search box for context menu click.
if (mouse_event.type() == ui::ET_MOUSE_PRESSED &&
mouse_event.IsOnlyRightMouseButton()) {
return false;
}
return SearchBoxViewBase::HandleMouseEvent(sender, mouse_event);
}
bool SearchBoxView::HandleGestureEvent(views::Textfield* sender,
const ui::GestureEvent& gesture_event) {
if (gesture_event.type() == ui::ET_GESTURE_TAP && HasAutocompleteText())
AcceptAutocompleteText();
return SearchBoxViewBase::HandleGestureEvent(sender, gesture_event);
}
void SearchBoxView::UpdateSearchBoxTextForSelectedResult(
SearchResult* selected_result) {
if (selected_result->result_type() ==
AppListSearchResultType::kInternalPrivacyInfo) {
// Privacy view should not change the search box text.
return;
}
if (selected_result->result_type() == AppListSearchResultType::kOmnibox &&
!selected_result->is_omnibox_search() &&
!selected_result->details().empty()) {
// If set, use details to ensure url results fill url.
search_box()->SetText(selected_result->details());
} else {
search_box()->SetText(selected_result->title());
}
}
void SearchBoxView::Update() {
search_box()->SetText(search_model_->search_box()->text());
UpdateButtonsVisisbility();
NotifyQueryChanged();
}
void SearchBoxView::SearchEngineChanged() {
UpdateSearchIcon();
}
void SearchBoxView::ShowAssistantChanged() {
if (search_model_) {
SetShowAssistantButton(
search_model_->search_box()->show_assistant_button());
}
}
bool SearchBoxView::ShouldProcessAutocomplete() {
// IME sets composition text while the user is typing, so avoid handle
// autocomplete in this case to avoid conflicts.
return is_app_list_search_autocomplete_enabled_ &&
!(search_box()->IsIMEComposing() && highlight_range_.is_empty());
}
void SearchBoxView::ResetHighlightRange() {
DCHECK(ShouldProcessAutocomplete());
const uint32_t text_length = search_box()->GetText().length();
highlight_range_.set_start(text_length);
highlight_range_.set_end(text_length);
}
void SearchBoxView::SetupAssistantButton() {
if (search_model_ && !search_model_->search_box()->show_assistant_button()) {
return;
}
views::ImageButton* assistant = assistant_button();
assistant->SetImage(
views::ImageButton::STATE_NORMAL,
gfx::CreateVectorIcon(chromeos::kAssistantIcon, kSearchBoxIconSize,
AppListColorProvider::Get()->GetSearchBoxIconColor(
gfx::kGoogleGrey700)));
base::string16 assistant_button_label(
l10n_util::GetStringUTF16(IDS_APP_LIST_START_ASSISTANT));
assistant->SetAccessibleName(assistant_button_label);
assistant->SetTooltipText(assistant_button_label);
}
} // namespace ash