// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ash/app_list/views/search_box_view.h"
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "ash/app_list/app_list_metrics.h"
#include "ash/app_list/app_list_model_provider.h"
#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/views/result_selection_controller.h"
#include "ash/app_list/views/search_box_view_delegate.h"
#include "ash/app_list/views/search_result_base_view.h"
#include "ash/ash_element_identifiers.h"
#include "ash/assistant/ui/main_stage/launcher_search_iph_view.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/keyboard/ui/keyboard_ui_controller.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/app_list_types.h"
#include "ash/public/cpp/app_list/vector_icons/vector_icons.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/wallpaper/wallpaper_types.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/search_box/search_box_constants.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/typography.h"
#include "ash/user_education/user_education_class_properties.h"
#include "ash/user_education/user_education_util.h"
#include "ash/user_education/welcome_tour/welcome_tour_metrics.h"
#include "base/containers/contains.h"
#include "base/i18n/case_conversion.h"
#include "base/i18n/rtl.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/rand_util.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "base/types/cxx23_to_underlying.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_enums.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/accessibility/ax_enums.mojom.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/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider_manager.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/paint_recorder.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/geometry/rounded_corners_f.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/animation/ink_drop_host.h"
#include "ui/views/animation/ink_drop_ripple.h"
#include "ui/views/background.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/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_model_adapter.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.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;
// Border insets for SearchBoxView in bubble launcher.
constexpr auto kBorderInsetsForAppListBubble = gfx::Insets::TLBR(4, 4, 4, 0);
// The default PlaceholderTextTypes used for productivity launcher. Randomly
// selected when placeholder text would be shown.
constexpr SearchBoxView::PlaceholderTextType kDefaultPlaceholders[] = {
SearchBoxView::PlaceholderTextType::kShortcuts,
SearchBoxView::PlaceholderTextType::kTabs,
SearchBoxView::PlaceholderTextType::kSettings,
SearchBoxView::PlaceholderTextType::kImages,
};
// PlaceholderTextTypes used for productivity launcher for cloud gaming devices.
// Randomly selected when placeholder text would be shown.
constexpr SearchBoxView::PlaceholderTextType kGamingPlaceholders[4] = {
SearchBoxView::PlaceholderTextType::kShortcuts,
SearchBoxView::PlaceholderTextType::kTabs,
SearchBoxView::PlaceholderTextType::kSettings,
SearchBoxView::PlaceholderTextType::kGames,
};
constexpr gfx::RoundedCornersF kAssistantButtonBackgroundRadiiLTR = {
18,
18,
4,
18,
};
constexpr gfx::RoundedCornersF kAssistantButtonBackgroundRadiiRTL = {
18,
18,
18,
4,
};
// List of all categories with their corresponding string id that would be shown
// in the menu.
constexpr auto kCategories =
base::MakeFixedFlatMap<AppListSearchControlCategory, int>(
{{AppListSearchControlCategory::kApps,
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_APPS},
{AppListSearchControlCategory::kAppShortcuts,
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_APP_SHORTCUTS},
{AppListSearchControlCategory::kFiles,
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_FILES},
{AppListSearchControlCategory::kGames,
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_GAMES},
{AppListSearchControlCategory::kHelp,
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_HELP},
{AppListSearchControlCategory::kImages,
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_IMAGES},
{AppListSearchControlCategory::kPlayStore,
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_PLAY_STORE},
{AppListSearchControlCategory::kWeb,
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_WEB}});
bool IsTrimmedQueryEmpty(const std::u16string& query) {
std::u16string trimmed_query;
base::TrimWhitespace(query, base::TrimPositions::TRIM_ALL, &trimmed_query);
return trimmed_query.empty();
}
std::u16string GetCategoryName(SearchResult* search_result) {
switch (search_result->category()) {
case ash::AppListSearchResultCategory::kApps:
return l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_APPS);
case ash::AppListSearchResultCategory::kAppShortcuts:
return l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_APP_SHORTCUTS);
case ash::AppListSearchResultCategory::kWeb:
return l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_WEB);
case ash::AppListSearchResultCategory::kFiles:
return (l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_FILES));
case ash::AppListSearchResultCategory::kSettings:
return l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_SETTINGS);
case ash::AppListSearchResultCategory::kHelp:
return l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_HELP);
case ash::AppListSearchResultCategory::kPlayStore:
return l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_PLAY_STORE);
case ash::AppListSearchResultCategory::kSearchAndAssistant:
return l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_SEARCH_AND_ASSISTANT);
case ash::AppListSearchResultCategory::kGames:
return l10n_util::GetStringUTF16(
IDS_ASH_SEARCH_RESULT_CATEGORY_LABEL_GAMES);
case ash::AppListSearchResultCategory::kUnknown:
return std::u16string();
}
}
std::u16string GetCategoryMenuItemTooltip(
AppListSearchControlCategory category) {
int tooltip_id = -1;
switch (category) {
case AppListSearchControlCategory::kApps:
tooltip_id = IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_APPS_TOOLTIP;
break;
case AppListSearchControlCategory::kAppShortcuts:
tooltip_id = IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_APP_SHORTCUTS_TOOLTIP;
break;
case AppListSearchControlCategory::kFiles:
tooltip_id = IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_FILES_TOOLTIP;
break;
case AppListSearchControlCategory::kGames:
tooltip_id = IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_GAMES_TOOLTIP;
break;
case AppListSearchControlCategory::kHelp:
tooltip_id = IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_HELP_TOOLTIP;
break;
case AppListSearchControlCategory::kImages:
tooltip_id = IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_IMAGES_TOOLTIP;
break;
case AppListSearchControlCategory::kPlayStore:
tooltip_id = IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_PLAYSTORE_TOOLTIP;
break;
case AppListSearchControlCategory::kWeb:
tooltip_id = IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_WEBSITES_TOOLTIP;
break;
case AppListSearchControlCategory::kCannotToggle:
NOTREACHED();
}
return l10n_util::GetStringUTF16(tooltip_id);
}
// Returns the check box icon that is shown on the category filter menu item.
ui::ImageModel GetCheckboxImage(bool checked) {
return ui::ImageModel::FromVectorIcon(
checked ? views::kCheckboxActiveIcon : views::kCheckboxNormalIcon,
checked ? cros_tokens::kCrosSysPrimary : cros_tokens::kCrosSysSecondary,
kAppContextMenuIconSize);
}
bool IsSubstringCaseInsensitive(std::u16string haystack_expr,
std::u16string needle_expr) {
// Convert complete given String to lower case
std::u16string haystack = base::i18n::ToLower(haystack_expr);
// Convert complete given Sub String to lower case
std::u16string needle = base::i18n::ToLower(needle_expr);
// Find substring in the given string
return base::Contains(haystack, needle);
}
void RecordAutocompleteMatchMetric(SearchBoxTextMatch match_type) {
base::UmaHistogramEnumeration("Apps.AppListSearchAutocomplete", match_type);
}
constexpr ui::ColorId GetFocusColorId() {
return cros_tokens::kCrosSysFocusRing;
}
class RoundRectPathGenerator : public views::HighlightPathGenerator {
public:
explicit RoundRectPathGenerator(const gfx::RoundedCornersF& radii)
: radii_(radii) {}
RoundRectPathGenerator(const RoundRectPathGenerator&) = delete;
RoundRectPathGenerator& operator=(const RoundRectPathGenerator&) = delete;
~RoundRectPathGenerator() override = default;
// views::HighlightPathGenerator:
std::optional<gfx::RRectF> GetRoundRect(const gfx::RectF& rect) override {
return gfx::RRectF(rect, radii_);
}
private:
const gfx::RoundedCornersF radii_;
};
} // namespace
class CheckBoxMenuItemView : public views::MenuItemView {
METADATA_HEADER(CheckBoxMenuItemView, views::MenuItemView)
public:
CheckBoxMenuItemView(views::MenuItemView* parent,
int command,
AppListViewDelegate* view_delegate)
: views::MenuItemView(parent,
command,
views::MenuItemView::Type::kNormal),
view_delegate_(view_delegate) {
// Set the role of the toggleable menu items to checkbox.
GetViewAccessibility().SetRole(ax::mojom::Role::kMenuItemCheckBox);
}
CheckBoxMenuItemView(const CheckBoxMenuItemView&) = delete;
CheckBoxMenuItemView& operator=(const CheckBoxMenuItemView&) = delete;
~CheckBoxMenuItemView() override = default;
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
views::MenuItemView::GetAccessibleNodeData(node_data);
// The title of the menu is not focusable but included in the position
// counting. Explicitly set the hierarchical level of the toggleable menu
// items to exclude the title.
node_data->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, 1);
}
void UpdateAccessibleCheckedState() override {
bool category_enabled = view_delegate_->IsCategoryEnabled(
static_cast<AppListSearchControlCategory>(GetCommand()));
GetViewAccessibility().SetCheckedState(
category_enabled ? ax::mojom::CheckedState::kTrue
: ax::mojom::CheckedState::kFalse);
}
private:
raw_ptr<AppListViewDelegate> view_delegate_ = nullptr;
};
BEGIN_METADATA(CheckBoxMenuItemView)
END_METADATA
class FilterMenuAdapter : public views::MenuModelAdapter {
public:
FilterMenuAdapter(ui::SimpleMenuModel* menu_model,
base::RepeatingClosure on_menu_closed,
AppListViewDelegate* view_delegate)
: views::MenuModelAdapter(menu_model, std::move(on_menu_closed)),
view_delegate_(view_delegate),
model_(menu_model) {}
FilterMenuAdapter(const FilterMenuAdapter&) = delete;
FilterMenuAdapter& operator=(const FilterMenuAdapter&) = delete;
~FilterMenuAdapter() override = default;
// Override AppendMenuItem to use customized MenuItemView.
views::MenuItemView* AppendMenuItem(views::MenuItemView* menu,
ui::MenuModel* model,
size_t model_index) override {
if (!menu->HasSubmenu()) {
menu->CreateSubmenu();
}
if (model->GetTypeAt(model_index) == ui::MenuModel::TYPE_TITLE) {
return menu->AppendTitle(model->GetLabelAt(model_index));
}
auto menu_item_view = std::make_unique<CheckBoxMenuItemView>(
menu, model->GetCommandIdAt(model_index), view_delegate_);
menu_item_view->SetTitle(model->GetLabelAt(model_index));
menu_item_view->SetIcon(model->GetIconAt(model_index));
menu_item_view->GetViewAccessibility().SetName(
model->GetAccessibleNameAt(model_index));
menu_item_view->UpdateAccessibleCheckedState();
const ui::ElementIdentifier element_id =
model->GetElementIdentifierAt(model_index);
if (element_id) {
menu_item_view->SetProperty(views::kElementIdentifierKey, element_id);
}
return menu->GetSubmenu()->AddChildView(std::move(menu_item_view));
}
// views::MenuDelegate
bool ShouldExecuteCommandWithoutClosingMenu(int id,
const ui::Event& e) override {
// Keep the menu open if the user toggles the checkboxes in the menu.
return true;
}
std::u16string GetTooltipText(int id,
const gfx::Point& screen_loc) const override {
if (id == ui::MenuModel::kTitleId) {
return std::u16string();
}
return GetCategoryMenuItemTooltip(
static_cast<AppListSearchControlCategory>(id));
}
void ExecuteCommand(int id) override { ExecuteCommand(id, 0); }
void ExecuteCommand(int id, int mouse_event_flags) override {
CHECK(id >= static_cast<int>(AppListSearchControlCategory::kMinValue) &&
id <= static_cast<int>(AppListSearchControlCategory::kMaxValue));
const auto category = static_cast<AppListSearchControlCategory>(id);
switch (category) {
case AppListSearchControlCategory::kApps:
case AppListSearchControlCategory::kAppShortcuts:
case AppListSearchControlCategory::kFiles:
case AppListSearchControlCategory::kGames:
case AppListSearchControlCategory::kHelp:
case AppListSearchControlCategory::kImages:
case AppListSearchControlCategory::kPlayStore:
case AppListSearchControlCategory::kWeb:
view_delegate_->SetCategoryEnabled(
category, !view_delegate_->IsCategoryEnabled(category));
break;
case AppListSearchControlCategory::kCannotToggle:
// There shouldn't be a "Cannot toggle" option.
NOTREACHED();
}
// Toggle the checkbox icon.
GetFilterMenuItemByCategory(category)->SetIcon(
GetCheckboxImage(view_delegate_->IsCategoryEnabled(category)));
GetFilterMenuItemByCategory(category)->UpdateAccessibleCheckedState();
}
void ShowFilterMenu(SearchBoxView* search_box) {
int run_types = views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
views::MenuRunner::FIXED_ANCHOR;
std::unique_ptr<views::MenuItemView> filter_menu_root = CreateMenu();
filter_menu_root_ = filter_menu_root.get();
filter_menu_runner_ = std::make_unique<views::MenuRunner>(
std::move(filter_menu_root), run_types);
filter_menu_runner_->RunMenuAt(
search_box->GetWidget(), nullptr /*button_controller*/,
search_box->filter_button()->GetBoundsInScreen(),
views::MenuAnchorPosition::kBubbleBottomRight,
ui::MenuSourceType::MENU_SOURCE_NONE);
}
// Returns true if the category filter menu is opened.
bool IsFilterMenuOpen() const {
return filter_menu_runner_ && filter_menu_runner_->IsRunning();
}
// Returns the menu item view in the category filter menu that indicates the
// `category` button. This should only be called when the menu is opened.
views::MenuItemView* GetFilterMenuItemByCategory(
AppListSearchControlCategory category) {
std::optional<size_t> index =
model_->GetIndexOfCommandId(base::to_underlying(category));
CHECK(index.has_value());
return GetFilterMenuItemByIdx(index.value());
}
private:
// Returns the menu item view at `index` in the category filter menu. This
// should only be called when the menu is opened.
views::MenuItemView* GetFilterMenuItemByIdx(int index) {
return filter_menu_root_->GetSubmenu()->GetMenuItemAt(index);
}
const raw_ptr<AppListViewDelegate> view_delegate_;
std::unique_ptr<views::MenuRunner> filter_menu_runner_;
raw_ptr<views::MenuItemView> filter_menu_root_;
raw_ptr<ui::SimpleMenuModel> model_;
};
class SearchBoxView::FocusRingLayer : public ui::LayerOwner, ui::LayerDelegate {
public:
FocusRingLayer()
: LayerOwner(std::make_unique<ui::Layer>(ui::LAYER_TEXTURED)) {
layer()->SetName("search_box/FocusRing");
layer()->SetFillsBoundsOpaquely(false);
layer()->set_delegate(this);
}
FocusRingLayer(const FocusRingLayer&) = delete;
FocusRingLayer& operator=(const FocusRingLayer&) = delete;
~FocusRingLayer() override = default;
void SetColor(SkColor color) {
if (color == color_) {
return;
}
color_ = color;
layer()->SchedulePaint(gfx::Rect(layer()->size()));
}
private:
// views::LayerDelegate:
void OnPaintLayer(const ui::PaintContext& context) override {
ui::PaintRecorder recorder(context, layer()->size());
gfx::Canvas* canvas = recorder.canvas();
// When using strokes to draw a rect, the bounds set is the center of the
// rect, which means that setting draw bounds to `bounds()` will leave half
// of the border outside the layer that may not be painted. Shrink the draw
// bounds by half of the width to solve this problem.
gfx::Rect draw_bounds(layer()->size());
draw_bounds.Inset(kSearchBoxFocusRingWidth / 2);
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(color_);
flags.setStyle(cc::PaintFlags::Style::kStroke_Style);
flags.setStrokeWidth(kSearchBoxFocusRingWidth);
canvas->DrawRoundRect(draw_bounds, kSearchBoxFocusRingCornerRadius, flags);
}
void OnDeviceScaleFactorChanged(float old_device_scale_factor,
float new_device_scale_factor) override {
layer()->SchedulePaint(gfx::Rect(layer()->size()));
}
SkColor color_ = gfx::kPlaceholderColor;
};
SearchBoxView::SearchBoxView(SearchBoxViewDelegate* delegate,
AppListViewDelegate* view_delegate,
bool is_app_list_bubble)
: delegate_(delegate),
view_delegate_(view_delegate),
is_app_list_bubble_(is_app_list_bubble) {
AppListModelProvider* const model_provider = AppListModelProvider::Get();
model_provider->AddObserver(this);
SearchBoxModel* const search_box_model =
model_provider->search_model()->search_box();
search_box_model_observer_.Observe(search_box_model);
// The assistant view delegate could be nullptr in test.
if (view_delegate_->GetAssistantViewDelegate()) {
assistant_view_delegate_observer_.Observe(
view_delegate_->GetAssistantViewDelegate());
}
if (features::IsUserEducationEnabled()) {
// NOTE: Set `kHelpBubbleContextKey` before `views::kElementIdentifierKey`
// in case registration causes a help bubble to be created synchronously.
SetProperty(kHelpBubbleContextKey, HelpBubbleContext::kAsh);
}
SetProperty(views::kElementIdentifierKey, kSearchBoxViewElementId);
auto font_list = TypographyProvider::Get()->ResolveTypographyToken(
TypographyToken::kCrosBody1);
SetPreferredStyleForSearchboxText(font_list, cros_tokens::kCrosSysOnSurface);
SetPreferredStyleForAutocompleteText(font_list,
cros_tokens::kCrosSysOnSurfaceVariant);
if (features::IsLauncherSearchControlEnabled()) {
views::ImageButton* filter_button = CreateFilterButton(base::BindRepeating(
&SearchBoxView::ShowFilterMenu, weak_ptr_factory_.GetWeakPtr()));
filter_button->SetFlipCanvasOnPaintForRTLUI(false);
std::u16string filter_button_label(
l10n_util::GetStringUTF16(IDS_ASH_SEARCH_BOX_FILTER_BUTTON_TOOLTIP));
filter_button->GetViewAccessibility().SetName(
l10n_util::GetStringUTF16(IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_TITLE));
filter_button->SetTooltipText(filter_button_label);
}
views::ImageButton* close_button = CreateCloseButton(base::BindRepeating(
&SearchBoxView::CloseButtonPressed, base::Unretained(this)));
std::u16string close_button_label(
l10n_util::GetStringUTF16(IDS_APP_LIST_CLEAR_SEARCHBOX));
close_button->GetViewAccessibility().SetName(close_button_label);
close_button->SetTooltipText(close_button_label);
CreateEndButtonContainer();
if (features::IsSunfishFeatureEnabled()) {
views::ImageButton* sunfish_button =
CreateSunfishButton(base::BindRepeating(
&SearchBoxView::SunfishButtonPressed, base::Unretained(this)));
sunfish_button->SetFlipCanvasOnPaintForRTLUI(false);
// TODO(http://b/361850292): Upload label for translation.
std::u16string sunfish_button_label(u"Select to search");
sunfish_button->GetViewAccessibility().SetName(sunfish_button_label);
sunfish_button->SetTooltipText(sunfish_button_label);
SetShowSunfishButton(true);
}
views::ImageButton* assistant_button =
CreateAssistantButton(base::BindRepeating(
&SearchBoxView::AssistantButtonPressed, base::Unretained(this)));
assistant_button->SetFlipCanvasOnPaintForRTLUI(false);
std::u16string assistant_button_label(
l10n_util::GetStringUTF16(IDS_APP_LIST_START_ASSISTANT));
assistant_button->GetViewAccessibility().SetName(assistant_button_label);
assistant_button->SetTooltipText(assistant_button_label);
SetShowAssistantButton(search_box_model->show_assistant_button());
GetViewAccessibility().SetRole(ax::mojom::Role::kTextField);
UpdateAccessibleValue();
}
SearchBoxView::~SearchBoxView() {
AppListModelProvider::Get()->RemoveObserver(this);
}
void SearchBoxView::InitializeForBubbleLauncher() {
SearchBoxViewBase::InitParams params;
params.show_close_button_when_active = false;
params.create_background = false;
params.animate_changing_search_icon = false;
params.increase_child_view_padding = true;
SearchBoxViewBase::Init(params);
UpdatePlaceholderTextAndAccessibleName();
}
void SearchBoxView::InitializeForFullscreenLauncher() {
SearchBoxViewBase::InitParams params;
params.show_close_button_when_active = true;
params.create_background = true;
params.animate_changing_search_icon = true;
SearchBoxViewBase::Init(params);
UpdatePlaceholderTextAndAccessibleName();
}
void SearchBoxView::SetResultSelectionController(
ResultSelectionController* controller) {
DCHECK(controller);
result_selection_controller_ = controller;
}
void SearchBoxView::ResetForShow() {
UpdateIphViewVisibility(false);
if (!is_search_box_active())
return;
ClearSearchAndDeactivateSearchBox();
}
void SearchBoxView::UpdateSearchTextfieldAccessibleActiveDescendantId() {
auto* const textfield = search_box();
if (!textfield) {
return;
}
if (a11y_active_descendant_) {
textfield->GetViewAccessibility().SetActiveDescendant(
*a11y_active_descendant_);
} else {
textfield->GetViewAccessibility().ClearActiveDescendant();
}
}
void SearchBoxView::OnActiveAppListModelsChanged(AppListModel* model,
SearchModel* search_model) {
search_box_model_observer_.Reset();
search_box_model_observer_.Observe(search_model->search_box());
ResetForShow();
UpdateSearchIcon();
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::HandleQueryChange(const std::u16string& query,
bool initiated_by_user) {
// Randomly select a new placeholder text when we get an empty new query.
if (query.empty()) {
UpdatePlaceholderTextAndAccessibleName();
}
MaybeSetAutocompleteGhostText(std::u16string(), std::u16string());
// Update autocomplete text highlight range to track user typed text.
if (ShouldProcessAutocomplete())
ResetHighlightRange();
if (initiated_by_user) {
const base::TimeTicks current_time = base::TimeTicks::Now();
if (current_query_.empty() && !query.empty()) {
base::RecordAction(base::UserMetricsAction("AppList_SearchQueryStarted"));
// Set 'user_initiated_model_update_time_' when initiating a new query.
user_initiated_model_update_time_ = current_time;
if (features::IsWelcomeTourEnabled()) {
welcome_tour_metrics::RecordInteraction(
user_education_util::GetLastActiveUserPrefService(),
welcome_tour_metrics::Interaction::kSearch);
}
} else if (!current_query_.empty() && query.empty()) {
base::RecordAction(base::UserMetricsAction("AppList_LeaveSearch"));
// Reset 'user_initiated_model_update_time_' when clearing the search_box.
user_initiated_model_update_time_ = base::TimeTicks();
} else if (query != current_query_ &&
!user_initiated_model_update_time_.is_null()) {
if (is_app_list_bubble_) {
UMA_HISTOGRAM_TIMES("Ash.SearchModelUpdateTime.ClamshellMode",
current_time - user_initiated_model_update_time_);
} else {
UMA_HISTOGRAM_TIMES("Ash.SearchModelUpdateTime.TabletMode",
current_time - user_initiated_model_update_time_);
}
user_initiated_model_update_time_ = current_time;
}
}
std::u16string trimmed_query;
base::TrimWhitespace(query, base::TrimPositions::TRIM_ALL, &trimmed_query);
const bool query_empty_changed =
trimmed_query.empty() != IsTrimmedQueryEmpty(current_query_);
current_query_ = query;
if (query_changed_callback_) {
query_changed_callback_.Run();
}
// Any query changes will dismiss the Launcher search IPH.
UpdateIphViewVisibility(false);
// The search box background depens on whether the query is empty, so schedule
// repaint when this changes.
if (query_empty_changed)
SchedulePaint();
delegate_->QueryChanged(trimmed_query, initiated_by_user);
// Don't reinitiate zero state search if the previous query was already empty
// (to avoid issuing zero state search twice in a row while clearing up search
// - see http://crbug.com/979594).
if (initiated_by_user || !trimmed_query.empty() || query_empty_changed)
view_delegate_->StartSearch(query);
}
void SearchBoxView::SetQueryChangedCallback(QueryChangedCallback callback) {
query_changed_callback_ = std::move(callback);
}
void SearchBoxView::UpdatePlaceholderTextStyle() {
SkColor primary_color =
GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurface);
SkColor secondary_color =
GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurfaceVariant);
if (is_app_list_bubble_) {
// The bubble launcher text is always side-aligned.
search_box()->set_placeholder_text_draw_flags(
base::i18n::IsRTL() ? gfx::Canvas::TEXT_ALIGN_RIGHT
: gfx::Canvas::TEXT_ALIGN_LEFT);
// Bubble launcher uses standard text colors (light-on-dark by default).
search_box()->set_placeholder_text_color(secondary_color);
return;
}
// Fullscreen launcher centers the text when inactive.
search_box()->set_placeholder_text_draw_flags(
is_search_box_active()
? (base::i18n::IsRTL() ? gfx::Canvas::TEXT_ALIGN_RIGHT
: gfx::Canvas::TEXT_ALIGN_LEFT)
: gfx::Canvas::TEXT_ALIGN_CENTER);
// Fullscreen launcher uses custom colors (dark-on-light by default).
search_box()->set_placeholder_text_color(
is_search_box_active() ? secondary_color : primary_color);
}
void SearchBoxView::UpdateSearchBoxBorder() {
gfx::Insets border_insets;
if (!is_app_list_bubble_) {
// Creates an empty border to create a region for the focus ring to appear.
border_insets = gfx::Insets(GetFocusRingSpacing());
} else {
// Bubble search box does not use a focus ring.
border_insets = kBorderInsetsForAppListBubble;
}
SetBorder(views::CreateEmptyBorder(border_insets));
}
void SearchBoxView::OnPaintBackground(gfx::Canvas* canvas) {
// Paint the SearchBoxBackground defined in SearchBoxViewBase first.
views::View::OnPaintBackground(canvas);
if (is_app_list_bubble_) {
// When the search box is focused, paint a vertical focus bar along the left
// edge, vertically aligned with the search icon.
if (search_box()->HasFocus() && IsTrimmedQueryEmpty(current_query_)) {
gfx::Point icon_origin;
views::View::ConvertPointToTarget(search_icon(), this, &icon_origin);
PaintFocusBar(canvas, gfx::Point(0, icon_origin.y()),
/*height=*/GetSearchBoxIconSize(),
GetColorProvider()->GetColor(GetFocusColorId()));
}
}
}
void SearchBoxView::OnPaintBorder(gfx::Canvas* canvas) {
if (should_paint_highlight_border_) {
views::HighlightBorder::PaintBorderToCanvas(
canvas, *this, GetContentsBounds(),
gfx::RoundedCornersF(corner_radius_),
chromeos::features::IsJellyrollEnabled()
? views::HighlightBorder::Type::kHighlightBorderNoShadow
: views::HighlightBorder::Type::kHighlightBorder1);
}
}
void SearchBoxView::OnThemeChanged() {
SearchBoxViewBase::OnThemeChanged();
const SkColor button_icon_color =
GetColorProvider()->GetColor(kColorAshButtonIconColor);
close_button()->SetImageModel(
views::ImageButton::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(views::kIcCloseIcon, button_icon_color,
GetSearchBoxIconSize()));
if (features::IsSunfishFeatureEnabled()) {
sunfish_button()->SetImageModel(
views::ImageButton::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(kSearchIcon, button_icon_color,
GetSearchBoxIconSize()));
}
assistant_button()->SetImageModel(
views::ImageButton::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(
chromeos::kAssistantIcon, button_icon_color, GetSearchBoxIconSize()));
if (filter_button()) {
filter_button()->SetImageModel(
views::ImageButton::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(kFilterIcon, button_icon_color,
GetSearchBoxIconSize()));
}
auto* focus_ring = views::FocusRing::Get(assistant_button());
focus_ring->SetOutsetFocusRingDisabled(true);
focus_ring->SetColorId(GetFocusColorId());
if (focus_ring_layer_) {
focus_ring_layer_->SetColor(
GetColorProvider()->GetColor(GetFocusColorId()));
}
UpdateSearchIcon();
UpdatePlaceholderTextStyle();
UpdateTextColor();
UpdateBackgroundColor(GetBackgroundColorForState(current_app_list_state_));
SchedulePaint();
}
void SearchBoxView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
if (focus_ring_layer_)
focus_ring_layer_->layer()->SetBounds(bounds());
}
void SearchBoxView::AddedToWidget() {
// Creating the search box focus ring relies on its parent layer which only
// exists after widget initialization.
if (!is_app_list_bubble_) {
focus_ring_layer_ = std::make_unique<FocusRingLayer>();
focus_ring_layer_->SetColor(
GetColorProvider()->GetColor(GetFocusColorId()));
layer()->parent()->Add(focus_ring_layer_->layer());
layer()->parent()->StackAtBottom(focus_ring_layer_->layer());
UpdateSearchBoxFocusPaint();
}
}
void SearchBoxView::RunLauncherSearchQuery(const std::u16string& query) {
UpdateQuery(query);
}
void SearchBoxView::OpenAssistantPage() {
UpdateIphViewVisibility(false);
view_delegate_->StartAssistant(
assistant::AssistantEntryPoint::kLauncherSearchIphChip);
}
void SearchBoxView::OnLauncherSearchChipPressed(const std::u16string& query) {
view_delegate_->EndAssistant(
assistant::AssistantExitPoint::kLauncherSearchIphChip);
UpdateQuery(query);
}
void SearchBoxView::ShowFilterMenu() {
filter_menu_adapter_ = nullptr;
ui::SimpleMenuModel* model = BuildFilterMenuModel();
filter_menu_adapter_ = std::make_unique<FilterMenuAdapter>(
model,
base::BindRepeating(&SearchBoxView::OnFilterMenuClosed,
weak_ptr_factory_.GetWeakPtr()),
view_delegate_);
filter_menu_adapter_->ShowFilterMenu(this);
RecordSearchCategoryFilterMenuOpened();
}
void SearchBoxView::OnFilterMenuClosed() {
// Trigger the search while keeping the same query text.
if (HasSearch()) {
TriggerSearch();
}
RecordSearchCategoryEnableState(GetSearchCategoryEnableState());
}
views::MenuItemView* SearchBoxView::GetFilterMenuItemByCategory(
AppListSearchControlCategory category) {
return filter_menu_adapter_->GetFilterMenuItemByCategory(category);
}
bool SearchBoxView::IsFilterMenuOpen() {
CHECK(filter_button());
return filter_menu_adapter_->IsFilterMenuOpen();
}
// static
int SearchBoxView::GetFocusRingSpacing() {
return kSearchBoxFocusRingWidth + kSearchBoxFocusRingPadding;
}
void SearchBoxView::OnSearchBoxActiveChanged(bool active) {
UpdateSearchIcon();
// Clear ghost text when toggling search box active state.
MaybeSetAutocompleteGhostText(std::u16string(), std::u16string());
if (active) {
result_selection_controller_->ResetSelection(nullptr,
true /* default_selection */);
} else {
result_selection_controller_->ClearSelection();
}
delegate_->ActiveChanged(this);
}
void SearchBoxView::UpdateSearchBoxFocusPaint() {
if (!focus_ring_layer_)
return;
// Paints the focus ring if the search box is focused.
if (search_box()->HasFocus() && !is_search_box_active() &&
view_delegate_->KeyboardTraversalEngaged()) {
focus_ring_layer_->layer()->SetVisible(true);
} else {
focus_ring_layer_->layer()->SetVisible(false);
}
}
void SearchBoxView::OnAfterUserAction(views::Textfield* sender) {
if (highlight_range_.length() > 0 &&
!search_box()->GetSelectedRange().EqualsIgnoringDirection(
highlight_range_)) {
ResetHighlightRange();
if (search_box()->GetSelectedRange().length() == 0 &&
current_query_ != search_box()->GetText()) {
RunLauncherSearchQuery(search_box()->GetText());
}
}
}
void SearchBoxView::OnKeyEvent(ui::KeyEvent* evt) {
// Only handle the key event that triggers the focus or result selection
// traversal here. Propagate the event to `delegate_` otherwise.
if (evt->type() != ui::EventType::kKeyPressed ||
!(IsUnhandledArrowKeyEvent(*evt) || evt->key_code() == ui::VKEY_TAB)) {
delegate_->OnSearchBoxKeyEvent(evt);
return;
}
const bool focus_on_close_button = close_button()->HasFocus();
const bool focus_on_filter_button =
filter_button() && filter_button()->HasFocus();
// Redirect the event handling if the focus is not on the buttons.
if (!focus_on_close_button && !focus_on_filter_button) {
delegate_->OnSearchBoxKeyEvent(evt);
return;
}
View* button_focused =
focus_on_close_button ? close_button() : filter_button();
bool moving_forward =
(evt->key_code() == ui::VKEY_TAB && !evt->IsShiftDown()) ||
evt->key_code() == ui::VKEY_DOWN || evt->key_code() == ui::VKEY_RIGHT;
View* next_view = GetFocusManager()->GetNextFocusableView(
button_focused, GetWidget(), /*reverse=*/!moving_forward,
/*dont_loop=*/true);
// Let FocusManager handles the focus change to its next focusable view if
// there is one, and let search result selection handle the focus if the
// next focusable view is the search box textfield.
if (next_view && next_view != search_box()) {
next_view->RequestFocus();
evt->SetHandled();
return;
}
// Check if the `delegate_` can handle the event if the focus is moving to
// result selection. All events focusing on the buttons should be handled now.
EnterSearchResultSelection(*evt);
evt->SetHandled();
}
void SearchBoxView::UpdateBackground(AppListState target_state) {
int corner_radius = GetSearchBoxBorderCornerRadiusForState(target_state);
SetSearchBoxBackgroundCornerRadius(corner_radius);
const bool is_corner_radius_changed = corner_radius_ != corner_radius;
corner_radius_ = corner_radius;
bool highlight_border_changed = false;
// The background layer is only painted for the search box in tablet mode.
// Also the layer is not painted when the search result page is visible.
if (!is_app_list_bubble_ && (!search_result_page_visible_ ||
target_state == AppListState::kStateApps)) {
layer()->SetClipRect(GetContentsBounds());
layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
layer()->SetRoundedCornerRadius(gfx::RoundedCornersF(corner_radius));
highlight_border_changed = !should_paint_highlight_border_;
should_paint_highlight_border_ = true;
} else {
layer()->SetBackgroundBlur(0);
layer()->SetBackdropFilterQuality(0);
highlight_border_changed = should_paint_highlight_border_;
should_paint_highlight_border_ = false;
}
if (is_corner_radius_changed || highlight_border_changed)
SchedulePaint();
UpdateBackgroundColor(GetBackgroundColorForState(target_state));
UpdateTextColor();
current_app_list_state_ = target_state;
}
void SearchBoxView::UpdateLayout(AppListState target_state,
int target_state_height) {
// Horizontal margins are selected to match search box icon's vertical
// margins. Space used for iph should be ignored.
const int iph_height =
GetIphView() ? GetIphView()->GetPreferredSize().height() : 0;
const int horizontal_spacing =
(target_state_height - iph_height - GetSearchBoxIconSize()) / 2;
const int horizontal_right_padding =
horizontal_spacing -
(GetSearchBoxButtonSize() - GetSearchBoxIconSize()) / 2;
box_layout_view()->SetInsideBorderInsets(
gfx::Insets::TLBR(0, horizontal_spacing, 0, horizontal_right_padding));
box_layout_view()->SetBetweenChildSpacing(horizontal_spacing);
InvalidateLayout();
// Avoid setting background when animating to kStateApps, background will be
// set when the animation ends.
if (target_state != AppListState::kStateApps)
UpdateBackground(target_state);
}
int SearchBoxView::GetSearchBoxBorderCornerRadiusForState(
AppListState state) const {
return state == AppListState::kStateSearchResults
? kExpandedSearchBoxCornerRadius
: kSearchBoxBorderCornerRadius;
}
SkColor SearchBoxView::GetBackgroundColorForState(AppListState state) const {
const auto* app_list_widget = GetWidget();
if (is_app_list_bubble_) {
return app_list_widget->GetColorProvider()->GetColor(
cros_tokens::kCrosSysSystemBaseElevated);
}
if (search_result_page_visible_)
return SK_ColorTRANSPARENT;
return app_list_widget->GetColorProvider()->GetColor(
cros_tokens::kCrosSysSystemBaseElevated);
}
void SearchBoxView::ProcessAutocomplete(
SearchResultBaseView* first_result_view) {
if (!ShouldProcessAutocomplete())
return;
if (!first_result_view || !first_result_view->selected())
return;
SearchResult* const first_visible_result = first_result_view->result();
// Do not autocomplete on answer cards.
if (!first_visible_result || first_visible_result->display_type() ==
SearchResultDisplayType::kAnswerCard) {
return;
}
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.
SetText(current_query_);
}
// Current non-autocompleted text.
const std::u16string& 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 std::u16string& details = first_visible_result->details();
const std::u16string& search_text = first_visible_result->title();
// Don't set autocomplete text if it's the same as user typed text.
if (user_typed_text == details || user_typed_text == search_text)
return;
if (ProcessPrefixMatchAutocomplete(first_visible_result, user_typed_text)) {
RecordAutocompleteMatchMetric(SearchBoxTextMatch::kPrefixMatch);
return;
}
// Clear autocomplete since we don't have a prefix match.
ClearAutocompleteText();
if (IsValidAutocompleteText(search_text)) {
// Setup autocomplete ghost text for eligible search_text.
MaybeSetAutocompleteGhostText(first_result_view->result()->title(),
GetCategoryName(first_result_view->result()));
if (IsSubstringCaseInsensitive(search_text, user_typed_text)) {
// user_typed_text is a substring of search_text and is eligible for
// autocompletion.
RecordAutocompleteMatchMetric(SearchBoxTextMatch::kSubstringMatch);
} else {
// user_typed_text does not match search_text but is eligible for
// autocompletion.
RecordAutocompleteMatchMetric(
SearchBoxTextMatch::kAutocompletedWithoutMatch);
}
} else {
// search_text is not eligible for autocompletion.
RecordAutocompleteMatchMetric(SearchBoxTextMatch::kNoMatch);
}
}
bool SearchBoxView::ProcessPrefixMatchAutocomplete(
SearchResult* search_result,
const std::u16string& user_typed_text) {
const std::u16string& details = search_result->details();
const std::u16string& search_text = search_result->title();
if (base::StartsWith(details, user_typed_text,
base::CompareCase::INSENSITIVE_ASCII) &&
IsValidAutocompleteText(details)) {
// Current text in the search_box matches the first result's url.
SetAutocompleteText(details);
MaybeSetAutocompleteGhostText(std::u16string(),
GetCategoryName(search_result));
return true;
}
if (base::StartsWith(search_text, user_typed_text,
base::CompareCase::INSENSITIVE_ASCII) &&
IsValidAutocompleteText(search_text)) {
// Current text in the search_box matches the first result's search result
// text.
SetAutocompleteText(search_text);
MaybeSetAutocompleteGhostText(std::u16string(),
GetCategoryName(search_result));
return true;
}
return false;
}
void SearchBoxView::ClearAutocompleteText() {
if (!ShouldProcessAutocomplete())
return;
// Clear ghost text.
MaybeSetAutocompleteGhostText(std::u16string(), std::u16string());
// 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::OnResultContainerVisibilityChanged(bool visible) {
if (search_result_page_visible_ == visible)
return;
search_result_page_visible_ = visible;
UpdateBackground(current_app_list_state_);
SchedulePaint();
}
bool SearchBoxView::HasValidQuery() {
return !IsTrimmedQueryEmpty(current_query_);
}
int SearchBoxView::GetSearchBoxIconSize() {
return kBubbleLauncherSearchBoxIconSize;
}
int SearchBoxView::GetSearchBoxButtonSize() {
return kBubbleLauncherSearchBoxButtonSizeDip;
}
void SearchBoxView::CloseButtonPressed() {
UpdateIphViewVisibility(false);
delegate_->CloseButtonPressed();
}
void SearchBoxView::AssistantButtonPressed() {
if (GetIphView()) {
// Notify the Assistant button is pressed when the IPH is visible and close
// the IPH.
GetIphView()->NotifyAssistantButtonPressedEvent();
UpdateIphViewVisibility(false);
delegate_->AssistantButtonPressed();
return;
}
// Tries to show an IPH. This can be rejected by various reasons.
UpdateIphViewVisibility(true);
// If UpdateIphViewVisibility() rejected the request, let the delegate_ handle
// this.
if (!GetIphView()) {
delegate_->AssistantButtonPressed();
return;
}
// Activate the search box based on UX SPEC.
SetSearchBoxActive(true, /*event_type=*/ui::EventType::kUnknown);
}
void SearchBoxView::SunfishButtonPressed() {
view_delegate_->DismissAppList();
CaptureModeController::Get()->StartSunfishSession();
}
void SearchBoxView::UpdateSearchIcon() {
const bool search_engine_is_google =
AppListModelProvider::Get()->search_model()->search_engine_is_google();
const gfx::VectorIcon& google_icon = is_search_box_active()
? vector_icons::kGoogleColorIcon
: kGoogleBlackIcon;
const gfx::VectorIcon& icon =
search_engine_is_google ? google_icon : kSearchEngineNotGoogleIcon;
SetSearchIconImage(gfx::CreateVectorIcon(
icon, GetSearchBoxIconSize(),
GetColorProvider()->GetColor(kColorAshButtonIconColor)));
}
bool SearchBoxView::IsValidAutocompleteText(
const std::u16string& autocomplete_text) {
// Don't set autocomplete text if it's the same as current search box
// text.
if (autocomplete_text == search_box()->GetText()) {
return false;
}
// Don't set autocomplete text if the highlighted text is the same as
// before.
if (autocomplete_text.length() > highlight_range_.start() &&
autocomplete_text.substr(highlight_range_.start()) ==
search_box()->GetSelectedText()) {
return false;
}
return true;
}
void SearchBoxView::UpdateTextColor() {
search_box()->SetTextColor(
GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurface));
}
void SearchBoxView::UpdatePlaceholderTextAndAccessibleName() {
const int a11y_name_template =
is_app_list_bubble_
? IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TEMPLATE_ACCESSIBILITY_NAME_CLAMSHELL
: IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TEMPLATE_ACCESSIBILITY_NAME_TABLET;
switch (SelectPlaceholderText()) {
case PlaceholderTextType::kShortcuts:
search_box()->SetPlaceholderText(l10n_util::GetStringFUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TEMPLATE,
l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_SHORTCUTS)));
search_box()->GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
a11y_name_template,
l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_SHORTCUTS)));
break;
case PlaceholderTextType::kTabs:
search_box()->SetPlaceholderText(l10n_util::GetStringFUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TEMPLATE,
l10n_util::GetStringUTF16(IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TABS)));
search_box()->GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
a11y_name_template,
l10n_util::GetStringUTF16(IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TABS)));
break;
case PlaceholderTextType::kSettings:
search_box()->SetPlaceholderText(l10n_util::GetStringFUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TEMPLATE,
l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_SETTINGS)));
search_box()->GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
a11y_name_template,
l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_SETTINGS)));
break;
case PlaceholderTextType::kGames:
search_box()->SetPlaceholderText(l10n_util::GetStringFUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TEMPLATE,
l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_GAMES)));
search_box()->GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
a11y_name_template, l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_GAMES)));
break;
case PlaceholderTextType::kImages:
search_box()->SetPlaceholderText(l10n_util::GetStringFUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_TEMPLATE,
l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_IMAGES)));
search_box()->GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
a11y_name_template, l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCH_BOX_PLACEHOLDER_IMAGES)));
break;
}
}
void SearchBoxView::AcceptAutocompleteText() {
if (!ShouldProcessAutocomplete())
return;
DCHECK(HasAutocompleteText());
search_box()->ClearSelection();
ResetHighlightRange();
RunLauncherSearchQuery(search_box()->GetText());
}
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::OnBeforeUserAction(views::Textfield* sender) {
if (a11y_active_descendant_)
SetA11yActiveDescendant(std::nullopt);
}
void SearchBoxView::SetAutocompleteText(
const std::u16string& autocomplete_text) {
if (!ShouldProcessAutocomplete())
return;
// Clear existing autocomplete text and reset the highlight range.
ClearAutocompleteText();
const std::u16string& current_text = search_box()->GetText();
// Currrent text is a prefix of autocomplete text.
DCHECK(base::StartsWith(autocomplete_text, current_text,
base::CompareCase::INSENSITIVE_ASCII));
// Autocomplete text should not be the same as current search box text.
DCHECK(autocomplete_text != current_text);
// Autocomplete text should not be the same as highlighted text.
// If the highlight range is beyond the suggested autocomplete text, clear
// the autocomplete ghost text and return.
if (highlight_range_.start() >= autocomplete_text.size()) {
MaybeSetAutocompleteGhostText(std::u16string(), std::u16string());
return;
}
const std::u16string& highlighted_text =
autocomplete_text.substr(highlight_range_.start());
DCHECK(highlighted_text != current_text);
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".
UpdateAccessibleValue();
MaybeSetAutocompleteGhostText(std::u16string(), std::u16string());
}
SearchBoxView::PlaceholderTextType SearchBoxView::SelectPlaceholderText()
const {
if (use_fixed_placeholder_text_for_test_)
return kDefaultPlaceholders[0];
if (chromeos::features::IsCloudGamingDeviceEnabled() ||
chromeos::features::IsAlmanacLauncherPayloadEnabled()) {
return kGamingPlaceholders[rand() % std::size(kGamingPlaceholders)];
}
return kDefaultPlaceholders[rand() % std::size(kDefaultPlaceholders)];
}
void SearchBoxView::UpdateQuery(const std::u16string& new_query) {
SetText(new_query);
ContentsChanged(search_box(), new_query);
}
void SearchBoxView::SetText(const std::u16string& text) {
search_box()->SetText(text);
UpdateAccessibleValue();
}
void SearchBoxView::EnterSearchResultSelection(const ui::KeyEvent& event) {
// If the focus on the button is going to move to the search box text field,
// ensure result selection gets updated according to the navigation key. The
// result selection is the reason navigation is handled by the search box
// instead of the focus manager - intended result selection depends on the key
// event that triggered the focus change.
search_box()->RequestFocus();
if (delegate_->CanSelectSearchResults() &&
result_selection_controller_->MoveSelection(event) ==
ResultSelectionController::MoveResult::kResultChanged) {
UpdateSearchBoxForSelectedResult(
result_selection_controller_->selected_result()->result());
}
}
void SearchBoxView::ClearSearchAndDeactivateSearchBox() {
UpdateIphViewVisibility(false);
if (!is_search_box_active())
return;
SetA11yActiveDescendant(std::nullopt);
// Set search box as inactive first, because ClearSearch() eventually calls
// into AppListMainView::QueryChanged() which will hide search results based
// on `is_search_box_active_`.
SetSearchBoxActive(false, ui::EventType::kUnknown);
ClearSearch();
MaybeSetAutocompleteGhostText(std::u16string(), std::u16string());
}
void SearchBoxView::SetA11yActiveDescendant(
const std::optional<ui::AXPlatformNodeId>& active_descendant) {
a11y_active_descendant_ = active_descendant;
UpdateSearchTextfieldAccessibleActiveDescendantId();
}
void SearchBoxView::UseFixedPlaceholderTextForTest() {
if (use_fixed_placeholder_text_for_test_)
return;
use_fixed_placeholder_text_for_test_ = true;
UpdatePlaceholderTextAndAccessibleName();
}
bool SearchBoxView::HandleKeyEvent(views::Textfield* sender,
const ui::KeyEvent& key_event) {
DCHECK(result_selection_controller_);
if (key_event.type() == ui::EventType::kKeyReleased) {
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 (!delegate_->CanSelectSearchResults())
return false;
// 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 (result_selection_controller_->MoveSelection(key_event) ==
ResultSelectionController::MoveResult::kResultChanged) {
UpdateSearchBoxForSelectedResult(
result_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 =
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 =
result_selection_controller_->selected_result();
if (selected_result) {
selected_result->OnKeyEvent(&event);
if (event.handled()) {
// Reset the selected result to the default result.
result_selection_controller_->ResetSelection(
nullptr, true /* default_selection */);
SetText(std::u16string());
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) &&
result_selection_controller_->selected_location_details() &&
result_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 =
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::
kSelectionCycleBeforeFirstResult:
// If the result selection was about to cycle, clear the selection and
// move the focus to the next element in the SearchBoxView -
// close_button().
if (HasAutocompleteText())
ClearAutocompleteText();
result_selection_controller_->ClearSelection();
// Check if the `delegate_` can handle the event if the focus is moving
// out from the result selection.
DCHECK(close_button()->GetVisible());
close_button()->RequestFocus();
SetA11yActiveDescendant(std::nullopt);
break;
case ResultSelectionController::MoveResult::kSelectionCycleAfterLastResult:
// If move was about to cycle, clear the selection and move the focus to
// the next element in the SearchBoxView - close_button() or
// filter_button().
if (HasAutocompleteText()) {
ClearAutocompleteText();
}
result_selection_controller_->ClearSelection();
if (filter_button()) {
filter_button()->RequestFocus();
} else {
close_button()->RequestFocus();
}
SetA11yActiveDescendant(std::nullopt);
break;
case ResultSelectionController::MoveResult::kResultChanged:
UpdateSearchBoxForSelectedResult(
result_selection_controller_->selected_result()->result());
break;
}
return true;
}
bool SearchBoxView::HandleMouseEvent(views::Textfield* sender,
const ui::MouseEvent& mouse_event) {
if (mouse_event.type() == ui::EventType::kMousePressed &&
HasAutocompleteText()) {
AcceptAutocompleteText();
}
// Don't activate search box for context menu click.
if (mouse_event.type() == ui::EventType::kMousePressed &&
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::EventType::kGestureTap &&
HasAutocompleteText()) {
AcceptAutocompleteText();
}
return SearchBoxViewBase::HandleGestureEvent(sender, gesture_event);
}
void SearchBoxView::UpdateSearchBoxForSelectedResult(
SearchResult* selected_result) {
if (!selected_result)
return;
if (selected_result->result_type() ==
AppListSearchResultType::kInternalPrivacyInfo ||
selected_result->display_type() == SearchResultDisplayType::kAnswerCard) {
// Privacy and answer card views should not change the search box text.
return;
}
ClearAutocompleteText();
const std::u16string& details = selected_result->details();
const std::u16string& search_text = selected_result->title();
// Don't set autocomplete text if it's the same as user typed text.
if (current_query_ == details || current_query_ == search_text) {
return;
}
if (!ProcessPrefixMatchAutocomplete(selected_result, current_query_)) {
MaybeSetAutocompleteGhostText(selected_result->title(),
GetCategoryName(selected_result));
}
}
void SearchBoxView::SearchEngineChanged() {
UpdateSearchIcon();
}
void SearchBoxView::ShowAssistantChanged() {
SetShowAssistantButton(AppListModelProvider::Get()
->search_model()
->search_box()
->show_assistant_button());
}
void SearchBoxView::UpdateIphViewVisibility(bool can_show_iph) {
const bool would_trigger_iph =
AppListModelProvider::Get()->search_model()->would_trigger_iph();
const bool is_iph_showing = GetIphView() != nullptr;
const bool should_show_iph = can_show_iph && would_trigger_iph;
if (should_show_iph == is_iph_showing) {
return;
}
if (should_show_iph) {
std::unique_ptr<ScopedIphSession> scoped_iph_session =
view_delegate_->CreateLauncherSearchIphSession();
if (!scoped_iph_session) {
return;
}
SetIphView(std::make_unique<LauncherSearchIphView>(
/*delegate=*/this, /*is_in_tablet_mode=*/!is_app_list_bubble_,
std::move(scoped_iph_session),
LauncherSearchIphView::UiLocation::kSearchBox));
auto radii = base::i18n::IsRTL() ? kAssistantButtonBackgroundRadiiRTL
: kAssistantButtonBackgroundRadiiLTR;
assistant_button()->SetBackground(views::CreateThemedRoundedRectBackground(
kColorAshControlBackgroundColorInactive, radii));
auto highlight_path_generator =
std::make_unique<RoundRectPathGenerator>(radii);
views::HighlightPathGenerator::Install(assistant_button(),
std::move(highlight_path_generator));
// The ink drop doesn't automatically pick up on rounded corner changes, so
// we need to manually notify it here.
views::InkDrop::Get(assistant_button())
->GetInkDrop()
->HostSizeChanged(assistant_button()->size());
// Update the focus ring.
views::FocusRing::Get(assistant_button())->SchedulePaint();
// Announce the IPH title.
GetViewAccessibility().AnnounceAlert(GetIphView()->GetTitleText());
} else {
DeleteIphView();
assistant_button()->SetBackground(nullptr);
views::InstallCircleHighlightPathGenerator(assistant_button());
}
// Adding or removing IPH view can change `SearchBoxView` bounds largely.
// Re-layout can be necessary on parent views as well. Explicitly call
// `InvalidateLayout` to trigger re-layouts on all parent views. Without this,
// we can have unnecessary spaces in `SearchBoxView` for an IPH dismiss under
// some conditions.
InvalidateLayout();
}
bool SearchBoxView::ShouldProcessAutocomplete() {
// IME sets composition text while the user is typing, so avoid handling
// autocomplete in this case to avoid conflicts.
// The user's cursor may not be at the end of the the current query, so avoid
// handling autocomplete in this case to avoid moving the user's cursor.
return search_box()->GetCursorPosition() == search_box()->GetText().size() &&
(!(search_box()->IsIMEComposing() && highlight_range_.is_empty()));
}
void SearchBoxView::ResetHighlightRange() {
const uint32_t text_length = search_box()->GetText().length();
highlight_range_.set_start(text_length);
highlight_range_.set_end(text_length);
}
ui::SimpleMenuModel* SearchBoxView::BuildFilterMenuModel() {
filter_menu_model_ = std::make_unique<ui::SimpleMenuModel>(nullptr);
filter_menu_model_->AddTitle(
l10n_util::GetStringUTF16(IDS_ASH_SEARCH_CATEGORY_FILTER_MENU_TITLE));
std::vector<AppListSearchControlCategory> available_categories =
GetToggleableCategories();
for (auto category : available_categories) {
if (category == AppListSearchControlCategory::kCannotToggle) {
continue;
}
filter_menu_model_->AddItemWithIcon(
static_cast<int>(category),
l10n_util::GetStringUTF16(kCategories.at(category)),
GetCheckboxImage(view_delegate_->IsCategoryEnabled(category)));
}
return filter_menu_model_.get();
}
std::vector<AppListSearchControlCategory>
SearchBoxView::GetToggleableCategories() {
return view_delegate_->GetToggleableCategories();
}
CategoryEnableStateMap SearchBoxView::GetSearchCategoryEnableState() {
auto toggleable_categories = GetToggleableCategories();
CategoryEnableStateMap category_to_state;
// Initialize the map.
for (int i = base::to_underlying(AppListSearchControlCategory::kMinValue);
i <= base::to_underlying(AppListSearchControlCategory::kMaxValue); ++i) {
auto category = static_cast<AppListSearchControlCategory>(i);
// Cannot toggle is not a category.
if (category == AppListSearchControlCategory::kCannotToggle) {
continue;
}
category_to_state[category] = SearchCategoryEnableState::kNotAvailable;
}
// Set the enable states for toggleable categories.
for (auto category : toggleable_categories) {
category_to_state[category] = view_delegate_->IsCategoryEnabled(category)
? SearchCategoryEnableState::kEnabled
: SearchCategoryEnableState::kDisabled;
}
return category_to_state;
}
void SearchBoxView::UpdateAccessibleValue() {
if (!HasAutocompleteText()) {
GetViewAccessibility().RemoveValue();
return;
}
GetViewAccessibility().SetValue(l10n_util::GetStringFUTF16(
IDS_APP_LIST_SEARCH_BOX_AUTOCOMPLETE, search_box()->GetText()));
}
BEGIN_METADATA(SearchBoxView)
END_METADATA
} // namespace ash