blob: 1bb3d14502b849625265fdfe9bbc2472a207bdc3 [file]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/quick_insert/views/quick_insert_view.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "ash/ash_element_identifiers.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/quick_insert/metrics/quick_insert_performance_metrics.h"
#include "ash/quick_insert/metrics/quick_insert_session_metrics.h"
#include "ash/quick_insert/model/quick_insert_action_type.h"
#include "ash/quick_insert/model/quick_insert_caps_lock_position.h"
#include "ash/quick_insert/model/quick_insert_mode_type.h"
#include "ash/quick_insert/model/quick_insert_search_results_section.h"
#include "ash/quick_insert/quick_insert_category.h"
#include "ash/quick_insert/quick_insert_search_result.h"
#include "ash/quick_insert/resources/grit/quick_insert_resources.h"
#include "ash/quick_insert/views/quick_insert_emoji_bar_view.h"
#include "ash/quick_insert/views/quick_insert_item_with_submenu_view.h"
#include "ash/quick_insert/views/quick_insert_key_event_handler.h"
#include "ash/quick_insert/views/quick_insert_main_container_view.h"
#include "ash/quick_insert/views/quick_insert_page_view.h"
#include "ash/quick_insert/views/quick_insert_positioning.h"
#include "ash/quick_insert/views/quick_insert_pseudo_focus.h"
#include "ash/quick_insert/views/quick_insert_search_bar_textfield.h"
#include "ash/quick_insert/views/quick_insert_search_field_view.h"
#include "ash/quick_insert/views/quick_insert_search_results_view.h"
#include "ash/quick_insert/views/quick_insert_search_results_view_delegate.h"
#include "ash/quick_insert/views/quick_insert_strings.h"
#include "ash/quick_insert/views/quick_insert_style.h"
#include "ash/quick_insert/views/quick_insert_submenu_controller.h"
#include "ash/quick_insert/views/quick_insert_submenu_view.h"
#include "ash/quick_insert/views/quick_insert_traversable_item_container.h"
#include "ash/quick_insert/views/quick_insert_view_delegate.h"
#include "ash/quick_insert/views/quick_insert_zero_state_view.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/branding_buildflags.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/aura/window.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/emoji/emoji_panel_helper.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/screen.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_observer.h"
#include "ui/views/view_tracker.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/frame_view.h"
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
#include "chromeos/ash/resources/internal/strings/grit/ash_internal_strings.h"
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
namespace ash {
namespace {
constexpr int kVerticalPaddingBetweenQuickInsertContainers = 8;
// Padding to separate the Quick Insert window from the screen edge.
constexpr gfx::Insets kPaddingFromScreenEdge(16);
std::unique_ptr<views::BubbleBorder> CreateBorder() {
auto border = std::make_unique<views::BubbleBorder>(
views::BubbleBorder::NONE, views::BubbleBorder::NO_SHADOW);
border->set_rounded_corners(
gfx::RoundedCornersF(kQuickInsertContainerBorderRadius));
border->SetColor(SK_ColorTRANSPARENT);
return border;
}
// Gets the preferred Quick Insert view bounds in screen coordinates. We try to
// place the Quick Insert view close to `anchor_bounds`, while taking into
// account `layout_type`, `quick_insert_view_size` and available space on the
// screen. `quick_insert_view_search_field_vertical_offset` is the vertical
// offset from the top of the Quick Insert view to the center of the search
// field, which we use to try to vertically align the search field with the
// center of the anchor bounds. `anchor_bounds` and returned bounds should be in
// screen coordinates.
gfx::Rect GetQuickInsertViewBoundsWithoutSelectedText(
const gfx::Rect& anchor_bounds,
QuickInsertLayoutType layout_type,
const gfx::Size& quick_insert_view_size,
int quick_insert_view_search_field_vertical_offset) {
gfx::Rect screen_work_area =
display::Screen::Get()->GetDisplayMatching(anchor_bounds).work_area();
screen_work_area.Inset(kPaddingFromScreenEdge);
gfx::Rect quick_insert_view_bounds(quick_insert_view_size);
if (anchor_bounds.right() + quick_insert_view_size.width() <=
screen_work_area.right()) {
// If there is space, place Quick Insert to the right of the anchor,
// vertically aligning the center of the Quick Insert search field with the
// center of the anchor.
quick_insert_view_bounds.set_origin(anchor_bounds.right_center());
quick_insert_view_bounds.Offset(
0, -quick_insert_view_search_field_vertical_offset);
} else {
switch (layout_type) {
case QuickInsertLayoutType::kMainResultsBelowSearchField:
// Try to place Quick Insert at the right edge of the screen, below
// the anchor.
quick_insert_view_bounds.set_origin(
{screen_work_area.right() - quick_insert_view_size.width(),
anchor_bounds.bottom()});
break;
case QuickInsertLayoutType::kMainResultsAboveSearchField:
// Try to place Quick Insert at the right edge of the screen, above
// the anchor.
quick_insert_view_bounds.set_origin(
{screen_work_area.right() - quick_insert_view_size.width(),
anchor_bounds.y() - quick_insert_view_size.height()});
break;
}
}
// Adjust if necessary to keep the whole Quick Insert view onscreen. Note that
// the non client area of Quick Insert, e.g. the shadows, are allowed to
// be offscreen.
quick_insert_view_bounds.AdjustToFit(screen_work_area);
return quick_insert_view_bounds;
}
// Gets the preferred Quick Insert view bounds in the case that there is
// selected text. We try to left align the Quick Insert view above or below
// `anchor_bounds`, while taking into account `layout_type`,
// `quick_insert_view_size` and available space on the screen. `anchor_bounds`
// and returned bounds should be in screen coordinates.
gfx::Rect GetQuickInsertViewBoundsWithSelectedText(
const gfx::Rect& anchor_bounds,
QuickInsertLayoutType layout_type,
const gfx::Size& quick_insert_view_size) {
gfx::Rect screen_work_area =
display::Screen::Get()->GetDisplayMatching(anchor_bounds).work_area();
screen_work_area.Inset(kPaddingFromScreenEdge);
gfx::Rect quick_insert_view_bounds(quick_insert_view_size);
switch (layout_type) {
case QuickInsertLayoutType::kMainResultsBelowSearchField:
// Left aligned below the anchor.
quick_insert_view_bounds.set_origin(
gfx::Point(anchor_bounds.x(), anchor_bounds.bottom()));
break;
case QuickInsertLayoutType::kMainResultsAboveSearchField:
// Left aligned above the anchor.
quick_insert_view_bounds.set_origin(
gfx::Point(anchor_bounds.x(),
anchor_bounds.y() - quick_insert_view_size.height()));
break;
}
// Adjust if necessary to keep the whole Quick Insert view onscreen.
quick_insert_view_bounds.AdjustToFit(screen_work_area);
return quick_insert_view_bounds;
}
QuickInsertCategory GetCategoryForMoreResults(QuickInsertSectionType type) {
switch (type) {
case QuickInsertSectionType::kNone:
case QuickInsertSectionType::kContentEditor:
case QuickInsertSectionType::kExamples:
case QuickInsertSectionType::kFeaturedGifs:
case QuickInsertSectionType::kSearchedGifs:
NOTREACHED();
case QuickInsertSectionType::kClipboard:
return QuickInsertCategory::kClipboard;
case QuickInsertSectionType::kLinks:
return QuickInsertCategory::kLinks;
case QuickInsertSectionType::kLocalFiles:
return QuickInsertCategory::kLocalFiles;
case QuickInsertSectionType::kDriveFiles:
return QuickInsertCategory::kDriveFiles;
}
}
std::u16string GetSearchFieldPlaceholderText(QuickInsertModeType mode,
bool is_editor_available) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
switch (mode) {
case QuickInsertModeType::kUnfocused:
return l10n_util::GetStringUTF16(
IDS_PICKER_SEARCH_FIELD_NO_FOCUS_PLACEHOLDER_TEXT);
case QuickInsertModeType::kNoSelection:
return l10n_util::GetStringUTF16(
is_editor_available
? IDS_PICKER_SEARCH_FIELD_NO_SELECTION_WITH_EDITOR_PLACEHOLDER_TEXT
: IDS_PICKER_SEARCH_FIELD_NO_SELECTION_PLACEHOLDER_TEXT);
case QuickInsertModeType::kHasSelection:
return l10n_util::GetStringUTF16(
is_editor_available
? IDS_PICKER_SEARCH_FIELD_HAS_SELECTION_WITH_EDITOR_PLACEHOLDER_TEXT
: IDS_PICKER_SEARCH_FIELD_HAS_SELECTION_PLACEHOLDER_TEXT);
default:
NOTREACHED();
}
#else
return u"Placeholder";
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
}
std::u16string GetNoResultsFoundDescription(QuickInsertCategory category) {
switch (category) {
case QuickInsertCategory::kLinks:
return l10n_util::GetStringUTF16(
IDS_PICKER_NO_RESULTS_FOR_BROWSING_HISTORY_LABEL_TEXT);
case QuickInsertCategory::kClipboard:
return l10n_util::GetStringUTF16(
IDS_PICKER_NO_RESULTS_FOR_CLIPBOARD_LABEL_TEXT);
case QuickInsertCategory::kDriveFiles:
return l10n_util::GetStringUTF16(
IDS_PICKER_NO_RESULTS_FOR_DRIVE_FILES_LABEL_TEXT);
case QuickInsertCategory::kLocalFiles:
return l10n_util::GetStringUTF16(
IDS_PICKER_NO_RESULTS_FOR_LOCAL_FILES_LABEL_TEXT);
case QuickInsertCategory::kDatesTimes:
case QuickInsertCategory::kUnitsMaths:
// TODO: b/345303965 - Add finalized strings for dates and maths.
return l10n_util::GetStringUTF16(IDS_PICKER_NO_RESULTS_TEXT);
case QuickInsertCategory::kEditorWrite:
case QuickInsertCategory::kEditorRewrite:
case QuickInsertCategory::kLobsterWithNoSelectedText:
case QuickInsertCategory::kLobsterWithSelectedText:
case QuickInsertCategory::kEmojisGifs:
case QuickInsertCategory::kEmojis:
NOTREACHED();
case QuickInsertCategory::kGifs:
// TODO: b/345303965 - Add finalized strings for GIFs.
return l10n_util::GetStringUTF16(IDS_PICKER_NO_RESULTS_TEXT);
}
}
ui::ImageModel GetNoResultsFoundIllustration() {
return ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
IDR_QUICK_INSERT_NO_RESULTS_ILLUSTRATION);
}
bool IsEditorAvailable(
base::span<const QuickInsertCategory> available_categories) {
return std::ranges::contains(available_categories,
QuickInsertCategory::kEditorWrite) ||
std::ranges::contains(available_categories,
QuickInsertCategory::kEditorRewrite);
}
} // namespace
QuickInsertView::QuickInsertView(QuickInsertViewDelegate* delegate,
const gfx::Rect& anchor_bounds,
QuickInsertLayoutType layout_type,
QuickInsertPositionType position_type,
const base::TimeTicks trigger_event_timestamp)
: performance_metrics_(trigger_event_timestamp), delegate_(delegate) {
SetShowCloseButton(false);
SetProperty(views::kElementIdentifierKey, kQuickInsertElementId);
// TODO: b/357991165 - The desired bounds delegate here is *not* used directly
// by the widget, because QuickInsertWidget does not use `autosize`. Rather,
// QuickInsertView manually calls GetDesiredWidgetBounds to adjust the Widget
// bounds to realign the search field with the caret position. Move this logic
// to a standalone class.
if (position_type == QuickInsertPositionType::kNearAnchor) {
set_desired_bounds_delegate(base::BindRepeating(
&QuickInsertView::GetTargetBounds, base::Unretained(this),
anchor_bounds, layout_type));
}
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::LayoutOrientation::kVertical,
/*inside_border_insets=*/gfx::Insets(),
/*between_child_spacing=*/kVerticalPaddingBetweenQuickInsertContainers));
AddMainContainerView(layout_type);
if (std::ranges::contains(delegate_->GetAvailableCategories(),
QuickInsertCategory::kEmojisGifs) ||
std::ranges::contains(delegate_->GetAvailableCategories(),
QuickInsertCategory::kEmojis)) {
AddEmojiBarView();
}
// Automatically focus on the search field.
SetInitiallyFocusedView(search_field_view_);
AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
AddAccelerator(ui::Accelerator(ui::VKEY_BROWSER_BACK, ui::EF_NONE));
key_event_handler_.SetActivePseudoFocusHandler(this);
pseudo_focused_view_tracker_.SetTrackEntireViewHierarchy(true);
preview_bubble_observation_.Observe(&preview_controller_);
}
QuickInsertView::~QuickInsertView() = default;
bool QuickInsertView::AcceleratorPressed(const ui::Accelerator& accelerator) {
switch (accelerator.key_code()) {
case ui::VKEY_ESCAPE:
if (preview_controller_.IsBubbleVisible()) {
preview_controller_.CloseBubble();
} else if (submenu_controller_.GetSubmenuView() != nullptr) {
submenu_controller_.Close();
} else if (auto* widget = GetWidget()) {
// Otherwise, close the Quick Insert widget.
widget->CloseWithReason(views::Widget::ClosedReason::kEscKeyPressed);
}
return true;
case ui::VKEY_BROWSER_BACK:
OnSearchBackButtonPressed();
return true;
default:
NOTREACHED();
}
}
std::unique_ptr<views::FrameView> QuickInsertView::CreateFrameView(
views::Widget* widget) {
auto frame =
std::make_unique<views::BubbleFrameView>(gfx::Insets(), gfx::Insets());
frame->SetBubbleBorder(CreateBorder());
return frame;
}
void QuickInsertView::AddedToWidget() {
performance_metrics_.StartRecording(*GetWidget());
// Due to layout considerations, only populate the emoji bar after the
// QuickInsertView has been added to a widget.
ResetEmojiBarToZeroState();
}
void QuickInsertView::RemovedFromWidget() {
performance_metrics_.StopRecording();
}
void QuickInsertView::Layout(PassKey) {
LayoutSuperclass<views::View>(this);
if (widget_bounds_needs_update_ && GetWidget() != nullptr) {
GetWidget()->SetBounds(GetDesiredWidgetBounds());
widget_bounds_needs_update_ = false;
}
}
void QuickInsertView::SelectZeroStateCategory(QuickInsertCategory category) {
SelectCategory(category);
}
void QuickInsertView::SelectZeroStateResult(
const QuickInsertSearchResult& result) {
SelectSearchResult(result);
}
QuickInsertActionType QuickInsertView::GetActionForResult(
const QuickInsertSearchResult& result) {
return delegate_->GetActionForResult(result);
}
void QuickInsertView::OnSearchResultsViewHeightChanged() {
SetWidgetBoundsNeedsUpdate();
}
void QuickInsertView::GetZeroStateSuggestedResults(
SuggestedResultsCallback callback) {
delegate_->GetZeroStateSuggestedResults(std::move(callback));
}
void QuickInsertView::RequestPseudoFocus(views::View* view) {
// Only allow `view` to become pseudo focused if it is visible and part of the
// active item container.
if (view == nullptr || !view->IsDrawn() ||
active_item_container_ == nullptr ||
!active_item_container_->ContainsItem(view)) {
return;
}
SetPseudoFocusedView(view);
}
void QuickInsertView::OnZeroStateViewHeightChanged() {
SetWidgetBoundsNeedsUpdate();
}
QuickInsertCapsLockPosition QuickInsertView::GetCapsLockPosition() {
return delegate_->GetCapsLockPosition();
}
void QuickInsertView::SetCapsLockDisplayed(bool displayed) {
delegate_->GetSessionMetrics().SetCapsLockDisplayed(displayed);
}
void QuickInsertView::SelectSearchResult(
const QuickInsertSearchResult& result) {
if (const QuickInsertCategoryResult* category_data =
std::get_if<QuickInsertCategoryResult>(&result)) {
SelectCategory(category_data->category);
} else if (const QuickInsertSearchRequestResult* search_request_data =
std::get_if<QuickInsertSearchRequestResult>(&result)) {
UpdateSearchQueryAndActivePage(search_request_data->primary_text);
} else if (const QuickInsertEditorResult* editor_data =
std::get_if<QuickInsertEditorResult>(&result)) {
delegate_->GetSessionMetrics().SetOutcome(
QuickInsertSessionMetrics::SessionOutcome::kRedirected);
delegate_->ShowEditor(
editor_data->preset_query_id,
base::UTF16ToUTF8(search_field_view_->GetQueryText()));
} else if (std::get_if<QuickInsertLobsterResult>(&result)) {
delegate_->GetSessionMetrics().SetOutcome(
QuickInsertSessionMetrics::SessionOutcome::kRedirected);
delegate_->ShowLobster(
base::UTF16ToUTF8(search_field_view_->GetQueryText()));
} else {
delegate_->GetSessionMetrics().SetSelectedResult(
result, search_results_view_->GetIndex(result));
switch (delegate_->GetActionForResult(result)) {
case QuickInsertActionType::kInsert:
delegate_->CloseWidgetThenInsertResultOnNextFocus(result);
break;
case QuickInsertActionType::kOpen:
case QuickInsertActionType::kDo:
delegate_->OpenResult(result);
GetWidget()->Close();
break;
case QuickInsertActionType::kCreate:
NOTREACHED();
}
}
}
void QuickInsertView::SelectMoreResults(QuickInsertSectionType type) {
SelectCategoryWithQuery(GetCategoryForMoreResults(type),
search_field_view_->GetQueryText());
}
void QuickInsertView::ToggleGifs(bool is_checked) {
if (base::FeatureList::IsEnabled(features::kPickerGifs)) {
is_gif_toggle_checked_ = is_checked;
if (is_gif_toggle_checked_) {
SelectCategoryWithQuery(QuickInsertCategory::kGifs,
search_field_view_->GetQueryText());
} else {
ResetSelectedCategory(/*reset_query=*/false);
}
} else {
ShowEmojiPicker(ui::EmojiPickerCategory::kGifs);
}
}
void QuickInsertView::ShowEmojiPicker(ui::EmojiPickerCategory category) {
QuickInsertSessionMetrics& session_metrics = delegate_->GetSessionMetrics();
session_metrics.SetSelectedCategory(QuickInsertCategory::kEmojisGifs);
session_metrics.SetOutcome(
QuickInsertSessionMetrics::SessionOutcome::kRedirected);
if (auto* widget = GetWidget()) {
widget->CloseWithReason(views::Widget::ClosedReason::kLostFocus);
}
delegate_->ShowEmojiPicker(category, search_field_view_->GetQueryText());
}
bool QuickInsertView::DoPseudoFocusedAction() {
if (clear_results_timer_.IsRunning()) {
// New results are still pending.
// TODO: b/351920494 - Insert the first new result instead of doing nothing.
return false;
}
if (auto* submenu_view = views::AsViewClass<QuickInsertItemWithSubmenuView>(
GetPseudoFocusedView())) {
submenu_view->ShowSubmenu();
SetPseudoFocusedView(submenu_controller_.GetSubmenuView()->GetTopItem());
return true;
}
return GetPseudoFocusedView() == nullptr
? false
: DoQuickInsertPseudoFocusedActionOnView(GetPseudoFocusedView());
}
bool QuickInsertView::MovePseudoFocusUp() {
if (views::View* item_above =
active_item_container_->GetItemAbove(GetPseudoFocusedView())) {
SetPseudoFocusedView(item_above);
} else {
AdvanceActiveItemContainer(QuickInsertPseudoFocusDirection::kBackward);
}
return true;
}
bool QuickInsertView::MovePseudoFocusDown() {
if (views::View* item_below =
active_item_container_->GetItemBelow(GetPseudoFocusedView())) {
SetPseudoFocusedView(item_below);
} else {
AdvanceActiveItemContainer(QuickInsertPseudoFocusDirection::kForward);
}
return true;
}
bool QuickInsertView::MovePseudoFocusLeft() {
views::View* pseudo_focused_view = GetPseudoFocusedView();
if (IsContainedInSubmenu(pseudo_focused_view)) {
SetPseudoFocusedView(submenu_controller_.GetAnchorView());
submenu_controller_.Close();
return true;
}
if (search_field_view_->Contains(pseudo_focused_view)) {
if (search_field_view_->LeftEventShouldMoveCursor(pseudo_focused_view)) {
return false;
}
views::View* left_view =
search_field_view_->GetViewLeftOf(pseudo_focused_view);
SetPseudoFocusedView(left_view);
search_field_view_->OnGainedPseudoFocusFromLeftEvent(left_view);
return true;
}
if (views::View* left_item =
active_item_container_->GetItemLeftOf(pseudo_focused_view)) {
SetPseudoFocusedView(left_item);
return true;
}
return false;
}
bool QuickInsertView::MovePseudoFocusRight() {
views::View* pseudo_focused_view = GetPseudoFocusedView();
if (views::IsViewClass<QuickInsertItemWithSubmenuView>(pseudo_focused_view)) {
views::AsViewClass<QuickInsertItemWithSubmenuView>(pseudo_focused_view)
->ShowSubmenu();
SetPseudoFocusedView(submenu_controller_.GetSubmenuView()->GetTopItem());
return true;
}
if (search_field_view_->Contains(pseudo_focused_view)) {
if (search_field_view_->RightEventShouldMoveCursor(pseudo_focused_view)) {
return false;
}
views::View* right_view =
search_field_view_->GetViewRightOf(pseudo_focused_view);
SetPseudoFocusedView(right_view);
search_field_view_->OnGainedPseudoFocusFromRightEvent(right_view);
return true;
}
if (views::View* right_item =
active_item_container_->GetItemRightOf(pseudo_focused_view)) {
SetPseudoFocusedView(right_item);
return true;
}
return false;
}
bool QuickInsertView::AdvancePseudoFocus(
QuickInsertPseudoFocusDirection direction) {
if (preview_controller_.IsBubbleVisible()) {
preview_controller_.CloseBubble();
}
if (GetPseudoFocusedView() == nullptr) {
return false;
}
SetPseudoFocusedView(GetNextQuickInsertPseudoFocusableView(
GetPseudoFocusedView(), direction, /*should_loop=*/true));
return true;
}
void QuickInsertView::OnPreviewBubbleVisibilityChanged(bool visible) {
if (views::Widget* widget = GetWidget()) {
// When the bubble is visible, turn off hiding the cursor on Esc key.
// If the cursor hides on Esc, the preview bubble is closed due to its
// OnMouseExit event handler, before QuickInsertView has a chance to handle
// the Esc key.
widget->GetNativeWindow()->SetProperty(kShowCursorOnKeypress, visible);
}
}
gfx::Rect QuickInsertView::GetTargetBounds(const gfx::Rect& anchor_bounds,
QuickInsertLayoutType layout_type) {
return delegate_->GetMode() == QuickInsertModeType::kHasSelection
? GetQuickInsertViewBoundsWithSelectedText(anchor_bounds,
layout_type, size())
: GetQuickInsertViewBoundsWithoutSelectedText(
anchor_bounds, layout_type, size(),
search_field_view_->bounds().CenterPoint().y() +
main_container_view_->bounds().y());
}
void QuickInsertView::UpdateSearchQueryAndActivePage(std::u16string query) {
search_field_view_->SetQueryText(std::move(query));
search_field_view_->RequestFocus();
UpdateActivePage();
}
void QuickInsertView::UpdateActivePage() {
std::u16string_view query =
base::TrimWhitespace(search_field_view_->GetQueryText(), base::TRIM_ALL);
if (query == last_query_ && selected_category_ == last_selected_category_ &&
is_gif_toggle_checked_ == last_is_gif_toggle_checked_) {
return;
}
last_query_ = std::u16string(query);
last_selected_category_ = selected_category_;
last_is_gif_toggle_checked_ = is_gif_toggle_checked_;
delegate_->GetSessionMetrics().UpdateSearchQuery(query);
if (!query.empty()) {
// Don't switch the active page immediately to the search view - this will
// be done when the clear results timer fires, or when
// `PublishSearchResults` is called.
clear_results_timer_.Start(
FROM_HERE, kClearResultsTimeout,
base::BindOnce(&QuickInsertView::OnClearResultsTimerFired,
weak_ptr_factory_.GetWeakPtr()));
delegate_->StartEmojiSearch(
query, base::BindOnce(&QuickInsertView::PublishEmojiResults,
weak_ptr_factory_.GetWeakPtr()));
delegate_->StartSearch(
query, selected_category_,
base::BindRepeating(&QuickInsertView::PublishSearchResults,
weak_ptr_factory_.GetWeakPtr()));
return;
}
if (selected_category_.has_value()) {
SetActivePage(category_results_view_);
if (last_suggested_results_category_ != selected_category_) {
// Getting suggested results for a category can be slow, so show a
// loading animation.
category_results_view_->ShowLoadingAnimation();
delegate_->GetResultsForCategory(
*selected_category_,
base::BindRepeating(&QuickInsertView::PublishCategoryResults,
weak_ptr_factory_.GetWeakPtr(),
*selected_category_));
last_suggested_results_category_ = selected_category_;
}
} else {
SetActivePage(zero_state_view_);
}
delegate_->StopSearch();
clear_results_timer_.Stop();
search_results_view_->ClearSearchResults();
ResetEmojiBarToZeroState();
}
void QuickInsertView::PublishEmojiResults(
std::vector<QuickInsertEmojiResult> results) {
if (emoji_bar_view_ == nullptr) {
return;
}
emoji_bar_view_->SetSearchResults(std::move(results));
search_results_view_->SetNumEmojiResultsForA11y(
emoji_bar_view_->GetNumItems());
}
void QuickInsertView::OnClearResultsTimerFired() {
// `QuickInsertView::UpdateActivePage` ensures that if the active page was set
// to the zero state or category view, the timer that this is called from is
// cancelled - which guarantees that this can't be called.
SetActivePage(search_results_view_);
search_results_view_->ClearSearchResults();
performance_metrics_.MarkSearchResultsUpdated(
QuickInsertPerformanceMetrics::SearchResultsUpdate::kClear);
}
void QuickInsertView::PublishSearchResults(
std::vector<QuickInsertSearchResultsSection> results) {
// `QuickInsertView::UpdateActivePage` ensures that if the active page was set
// to the zero state or category view, the delegate's search is stopped -
// which guarantees that this can't be called.
SetActivePage(search_results_view_);
bool clear_stale_results = clear_results_timer_.IsRunning();
if (clear_stale_results) {
clear_results_timer_.Stop();
search_results_view_->ClearSearchResults();
}
if (results.empty()) {
bool no_results_found_shown = search_results_view_->SearchStopped(
/*illustration=*/{},
l10n_util::GetStringUTF16(IDS_PICKER_NO_RESULTS_TEXT));
if (no_results_found_shown) {
performance_metrics_.MarkSearchResultsUpdated(
QuickInsertPerformanceMetrics::SearchResultsUpdate::kNoResultsFound);
} else {
CHECK(!clear_stale_results)
<< "Stale results were cleared when no results were found, but the "
"\"no results found\" screen was not shown";
// `clear_stale_results` must be false here, so nothing happened.
}
return;
}
for (QuickInsertSearchResultsSection& result : results) {
search_results_view_->AppendSearchResults(std::move(result));
}
QuickInsertPerformanceMetrics::SearchResultsUpdate update;
if (clear_stale_results) {
update = QuickInsertPerformanceMetrics::SearchResultsUpdate::kReplace;
} else {
update = QuickInsertPerformanceMetrics::SearchResultsUpdate::kAppend;
}
performance_metrics_.MarkSearchResultsUpdated(update);
}
void QuickInsertView::SelectCategory(QuickInsertCategory category) {
SelectCategoryWithQuery(category, /*query=*/u"");
}
void QuickInsertView::SelectCategoryWithQuery(QuickInsertCategory category,
std::u16string_view query) {
QuickInsertSessionMetrics& session_metrics = delegate_->GetSessionMetrics();
session_metrics.SetSelectedCategory(category);
selected_category_ = category;
if (category == QuickInsertCategory::kEmojisGifs ||
category == QuickInsertCategory::kEmojis) {
session_metrics.SetOutcome(
QuickInsertSessionMetrics::SessionOutcome::kRedirected);
if (auto* widget = GetWidget()) {
// TODO(b/316936394): Correctly handle opening of emoji picker. Probably
// best to wait for the IME on focus event, or save some coordinates and
// open emoji picker in the correct location in some other way.
widget->CloseWithReason(views::Widget::ClosedReason::kLostFocus);
}
delegate_->ShowEmojiPicker(ui::EmojiPickerCategory::kEmojis, query);
return;
}
if (category == QuickInsertCategory::kEditorWrite ||
category == QuickInsertCategory::kEditorRewrite) {
session_metrics.SetOutcome(
QuickInsertSessionMetrics::SessionOutcome::kRedirected);
if (auto* widget = GetWidget()) {
widget->CloseWithReason(views::Widget::ClosedReason::kLostFocus);
}
CHECK(query.empty());
delegate_->ShowEditor(/*preset_query_id*/ std::nullopt,
/*freeform_text=*/std::nullopt);
return;
}
if (category == QuickInsertCategory::kLobsterWithNoSelectedText ||
category == QuickInsertCategory::kLobsterWithSelectedText) {
session_metrics.SetOutcome(
QuickInsertSessionMetrics::SessionOutcome::kRedirected);
if (auto* widget = GetWidget()) {
widget->CloseWithReason(views::Widget::ClosedReason::kLostFocus);
}
delegate_->ShowLobster(/*query=*/std::nullopt);
return;
}
search_field_view_->SetPlaceholderText(
GetSearchFieldPlaceholderTextForQuickInsertCategory(category));
search_field_view_->SetBackButtonVisible(category !=
QuickInsertCategory::kGifs);
SetEmojiBarVisibleIfEnabled(category == QuickInsertCategory::kGifs);
UpdateSearchQueryAndActivePage(std::u16string(query));
}
void QuickInsertView::PublishCategoryResults(
QuickInsertCategory category,
std::vector<QuickInsertSearchResultsSection> results) {
category_results_view_->ClearSearchResults();
for (QuickInsertSearchResultsSection& section : results) {
if (!section.results().empty()) {
category_results_view_->AppendSearchResults(std::move(section));
}
}
category_results_view_->SearchStopped(GetNoResultsFoundIllustration(),
GetNoResultsFoundDescription(category));
}
void QuickInsertView::AddMainContainerView(QuickInsertLayoutType layout_type) {
main_container_view_ =
AddChildView(std::make_unique<QuickInsertMainContainerView>());
// `base::Unretained` is safe here because this class owns
// `main_container_view_`, which owns `search_field_view_`.
search_field_view_ = main_container_view_->AddSearchFieldView(
views::Builder<QuickInsertSearchFieldView>(
std::make_unique<QuickInsertSearchFieldView>(
base::IgnoreArgs<const std::u16string&>(base::BindRepeating(
&QuickInsertView::UpdateActivePage, base::Unretained(this))),
base::BindRepeating(&QuickInsertView::OnSearchBackButtonPressed,
base::Unretained(this)),
&key_event_handler_, &performance_metrics_))
.SetPlaceholderText(GetSearchFieldPlaceholderText(
delegate_->GetMode(),
IsEditorAvailable(delegate_->GetAvailableCategories())))
.Build());
main_container_view_->AddContentsView(layout_type);
zero_state_view_ =
main_container_view_->AddPage(std::make_unique<QuickInsertZeroStateView>(
this, delegate_->GetAvailableCategories(), kQuickInsertViewWidth,
delegate_->GetAssetFetcher(), &submenu_controller_,
&preview_controller_));
category_results_view_ = main_container_view_->AddPage(
std::make_unique<QuickInsertSearchResultsView>(
this, kQuickInsertViewWidth, delegate_->GetAssetFetcher(),
&submenu_controller_, &preview_controller_));
category_results_view_->SetLocalFileResultStyle(
QuickInsertSearchResultsView::LocalFileResultStyle::kGrid);
search_results_view_ = main_container_view_->AddPage(
std::make_unique<QuickInsertSearchResultsView>(
this, kQuickInsertViewWidth, delegate_->GetAssetFetcher(),
&submenu_controller_, &preview_controller_));
SetActivePage(zero_state_view_);
}
void QuickInsertView::AddEmojiBarView() {
emoji_bar_view_ =
AddChildViewAt(std::make_unique<QuickInsertEmojiBarView>(
this, kQuickInsertViewWidth,
/*is_gifs_enabled*/ delegate_->IsGifsEnabled()),
0);
}
void QuickInsertView::SetActivePage(QuickInsertPageView* page_view) {
main_container_view_->SetActivePage(page_view);
SetPseudoFocusedView(nullptr);
active_item_container_ = page_view;
SetPseudoFocusedView(active_item_container_->GetTopItem());
SetWidgetBoundsNeedsUpdate();
}
void QuickInsertView::SetEmojiBarVisibleIfEnabled(bool visible) {
if (emoji_bar_view_ == nullptr) {
return;
}
emoji_bar_view_->SetVisible(visible);
SetWidgetBoundsNeedsUpdate();
}
void QuickInsertView::AdvanceActiveItemContainer(
QuickInsertPseudoFocusDirection direction) {
if (active_item_container_ == submenu_controller_.GetSubmenuView()) {
// Just keep the submenu as the active item container.
} else if (emoji_bar_view_ == nullptr ||
active_item_container_ == emoji_bar_view_) {
active_item_container_ = main_container_view_;
} else {
active_item_container_ = emoji_bar_view_;
}
SetPseudoFocusedView(direction == QuickInsertPseudoFocusDirection::kForward
? active_item_container_->GetTopItem()
: active_item_container_->GetBottomItem());
}
void QuickInsertView::SetPseudoFocusedView(views::View* view) {
if (view == nullptr) {
SetPseudoFocusedView(search_field_view_->textfield());
return;
}
if (pseudo_focused_view_tracker_.view() == view) {
return;
}
if (IsContainedInSubmenu(view)) {
active_item_container_ = submenu_controller_.GetSubmenuView();
} else {
submenu_controller_.Close();
if (emoji_bar_view_ != nullptr && emoji_bar_view_->Contains(view)) {
active_item_container_ = emoji_bar_view_;
} else {
active_item_container_ = main_container_view_;
}
}
RemoveQuickInsertPseudoFocusFromView(pseudo_focused_view_tracker_.view());
pseudo_focused_view_tracker_.SetView(view);
// base::Unretained() is safe here because this class owns
// `pseudo_focused_view_tracker_`.
pseudo_focused_view_tracker_.SetIsDeletingCallback(base::BindOnce(
&QuickInsertView::SetPseudoFocusedView, base::Unretained(this), nullptr));
search_field_view_->SetTextfieldActiveDescendant(view);
view->ScrollViewToVisible();
ApplyQuickInsertPseudoFocusToView(view);
}
views::View* QuickInsertView::GetPseudoFocusedView() {
return pseudo_focused_view_tracker_.view();
}
void QuickInsertView::ResetSelectedCategory(bool reset_query) {
search_field_view_->SetPlaceholderText(GetSearchFieldPlaceholderText(
delegate_->GetMode(),
IsEditorAvailable(delegate_->GetAvailableCategories())));
search_field_view_->SetBackButtonVisible(false);
SetEmojiBarVisibleIfEnabled(true);
selected_category_ = std::nullopt;
if (reset_query) {
UpdateSearchQueryAndActivePage(u"");
} else {
UpdateActivePage();
}
}
void QuickInsertView::OnSearchBackButtonPressed() {
ResetSelectedCategory(/*reset_query=*/true);
CHECK_EQ(main_container_view_->active_page(), zero_state_view_)
<< "UpdateSearchQueryAndActivePage did not set active page to zero state "
"view";
}
void QuickInsertView::ResetEmojiBarToZeroState() {
if (emoji_bar_view_ == nullptr) {
return;
}
emoji_bar_view_->SetSearchResults(delegate_->GetSuggestedEmoji());
}
bool QuickInsertView::IsContainedInSubmenu(views::View* view) {
return submenu_controller_.GetSubmenuView() != nullptr &&
submenu_controller_.GetSubmenuView()->Contains(view);
}
void QuickInsertView::SetWidgetBoundsNeedsUpdate() {
widget_bounds_needs_update_ = true;
}
BEGIN_METADATA(QuickInsertView)
END_METADATA
} // namespace ash