// 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/picker/views/picker_zero_state_view.h"
#include <cstddef>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "ash/picker/metrics/picker_session_metrics.h"
#include "ash/picker/model/picker_caps_lock_position.h"
#include "ash/picker/picker_asset_fetcher.h"
#include "ash/picker/picker_clipboard_history_provider.h"
#include "ash/picker/views/picker_category_type.h"
#include "ash/picker/views/picker_icons.h"
#include "ash/picker/views/picker_item_view.h"
#include "ash/picker/views/picker_item_with_submenu_view.h"
#include "ash/picker/views/picker_list_item_view.h"
#include "ash/picker/views/picker_preview_bubble_controller.h"
#include "ash/picker/views/picker_pseudo_focus.h"
#include "ash/picker/views/picker_section_list_view.h"
#include "ash/picker/views/picker_section_view.h"
#include "ash/picker/views/picker_strings.h"
#include "ash/picker/views/picker_traversable_item_container.h"
#include "ash/picker/views/picker_zero_state_view_delegate.h"
#include "ash/public/cpp/picker/picker_category.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "chromeos/components/editor_menu/public/cpp/preset_text_query.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/image/image.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/animation/bounds_animator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/layout_manager.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
namespace ash {
namespace {
enum class EditorSubmenu { kNone, kLength, kTone };
constexpr base::TimeDelta kCapsLockDisplayDelay = base::Milliseconds(50);
EditorSubmenu GetEditorSubmenu(
std::optional<chromeos::editor_menu::PresetQueryCategory> category) {
if (!category.has_value()) {
return EditorSubmenu::kNone;
}
switch (*category) {
case chromeos::editor_menu::PresetQueryCategory::kUnknown:
return EditorSubmenu::kNone;
case chromeos::editor_menu::PresetQueryCategory::kShorten:
return EditorSubmenu::kLength;
case chromeos::editor_menu::PresetQueryCategory::kElaborate:
return EditorSubmenu::kLength;
case chromeos::editor_menu::PresetQueryCategory::kRephrase:
return EditorSubmenu::kNone;
case chromeos::editor_menu::PresetQueryCategory::kFormalize:
return EditorSubmenu::kTone;
case chromeos::editor_menu::PresetQueryCategory::kEmojify:
return EditorSubmenu::kTone;
case chromeos::editor_menu::PresetQueryCategory::kProofread:
return EditorSubmenu::kNone;
}
}
} // namespace
PickerZeroStateView::PickerZeroStateView(
PickerZeroStateViewDelegate* delegate,
base::span<const PickerCategory> available_categories,
int picker_view_width,
PickerAssetFetcher* asset_fetcher,
PickerSubmenuController* submenu_controller,
PickerPreviewBubbleController* preview_controller)
: delegate_(delegate),
submenu_controller_(submenu_controller),
preview_controller_(preview_controller) {
SetLayoutManager(std::make_unique<views::BoxLayout>())
->SetOrientation(views::LayoutOrientation::kVertical);
section_list_view_ = AddChildView(std::make_unique<PickerSectionListView>(
picker_view_width, asset_fetcher, submenu_controller_));
for (PickerCategory category : available_categories) {
// kEditorRewrite is not visible in the zero-state, since it's replaced with
// the rewrite suggestions.
if (category == PickerCategory::kEditorRewrite) {
continue;
}
auto result = PickerCategoryResult(category);
GetOrCreateSectionView(category)->AddResult(
std::move(result), preview_controller_,
PickerSectionView::LocalFileResultStyle::kList,
base::BindRepeating(&PickerZeroStateView::OnCategorySelected,
weak_ptr_factory_.GetWeakPtr(), category));
}
delegate_->GetZeroStateSuggestedResults(
base::BindRepeating(&PickerZeroStateView::OnFetchSuggestedResults,
weak_ptr_factory_.GetWeakPtr()));
delegate_->OnZeroStateViewHeightChanged();
}
PickerZeroStateView::~PickerZeroStateView() = default;
views::View* PickerZeroStateView::GetTopItem() {
return section_list_view_->GetTopItem();
}
views::View* PickerZeroStateView::GetBottomItem() {
return section_list_view_->GetBottomItem();
}
views::View* PickerZeroStateView::GetItemAbove(views::View* item) {
if (!Contains(item)) {
return nullptr;
}
if (views::IsViewClass<PickerItemView>(item)) {
// Skip views that aren't PickerItemViews, to allow users to quickly
// navigate between items.
return section_list_view_->GetItemAbove(item);
}
views::View* prev_item = GetNextPickerPseudoFocusableView(
item, PickerPseudoFocusDirection::kBackward, /*should_loop=*/false);
return Contains(prev_item) ? prev_item : nullptr;
}
views::View* PickerZeroStateView::GetItemBelow(views::View* item) {
if (!Contains(item)) {
return nullptr;
}
if (views::IsViewClass<PickerItemView>(item)) {
// Skip views that aren't PickerItemViews, to allow users to quickly
// navigate between items.
return section_list_view_->GetItemBelow(item);
}
views::View* next_item = GetNextPickerPseudoFocusableView(
item, PickerPseudoFocusDirection::kForward, /*should_loop=*/false);
return Contains(next_item) ? next_item : nullptr;
}
views::View* PickerZeroStateView::GetItemLeftOf(views::View* item) {
if (!Contains(item) || !views::IsViewClass<PickerItemView>(item)) {
return nullptr;
}
return section_list_view_->GetItemLeftOf(item);
}
views::View* PickerZeroStateView::GetItemRightOf(views::View* item) {
if (!Contains(item) || !views::IsViewClass<PickerItemView>(item)) {
return nullptr;
}
return section_list_view_->GetItemRightOf(item);
}
bool PickerZeroStateView::ContainsItem(views::View* item) {
return Contains(item);
}
PickerSectionView* PickerZeroStateView::GetOrCreateSectionView(
PickerCategoryType category_type) {
auto section_view_iterator = category_section_views_.find(category_type);
if (section_view_iterator != category_section_views_.end()) {
return section_view_iterator->second;
}
auto* section_view = section_list_view_->AddSection();
section_view->AddTitleLabel(
GetSectionTitleForPickerCategoryType(category_type));
category_section_views_.insert({category_type, section_view});
return section_view;
}
PickerSectionView* PickerZeroStateView::GetOrCreateSectionView(
PickerCategory category) {
return GetOrCreateSectionView(GetPickerCategoryType(category));
}
void PickerZeroStateView::OnCategorySelected(PickerCategory category) {
delegate_->SelectZeroStateCategory(category);
}
void PickerZeroStateView::OnResultSelected(const PickerSearchResult& result) {
delegate_->SelectZeroStateResult(result);
}
void PickerZeroStateView::AddResultToSection(const PickerSearchResult& result,
PickerSectionView* section) {
PickerItemView* view = section->AddResult(
result, preview_controller_,
PickerSectionView::LocalFileResultStyle::kList,
base::BindRepeating(&PickerZeroStateView::OnResultSelected,
weak_ptr_factory_.GetWeakPtr(), result));
if (auto* list_item_view = views::AsViewClass<PickerListItemView>(view)) {
list_item_view->SetBadgeAction(delegate_->GetActionForResult(result));
}
}
void PickerZeroStateView::OnFetchSuggestedResults(
std::vector<PickerSearchResult> results) {
if (results.empty()) {
return;
}
// TODO: b/343092747 - Remove this to the top once the `primary_section_view_`
// always has at least one child.
if (primary_section_view_ == nullptr) {
primary_section_view_ = section_list_view_->AddSectionAt(0);
}
auto new_window_submenu =
views::Builder<PickerItemWithSubmenuView>()
.SetSubmenuController(submenu_controller_)
.SetText(l10n_util::GetStringUTF16(IDS_PICKER_NEW_MENU_LABEL))
.SetLeadingIcon(ui::ImageModel::FromVectorIcon(
kSystemMenuPlusIcon, cros_tokens::kCrosSysOnSurface))
.Build();
// Some Editor results are shown directly in the section, some shown behind
// submenus. Iterate through the results and put them into the right View.
auto length_submenu =
views::Builder<PickerItemWithSubmenuView>()
.SetSubmenuController(submenu_controller_)
.SetText(
l10n_util::GetStringUTF16(IDS_PICKER_CHANGE_LENGTH_MENU_LABEL))
.SetLeadingIcon(ui::ImageModel::FromVectorIcon(
chromeos::kEditorMenuShortenIcon, cros_tokens::kCrosSysOnSurface))
.Build();
auto tone_submenu =
views::Builder<PickerItemWithSubmenuView>()
.SetSubmenuController(submenu_controller_)
.SetText(l10n_util::GetStringUTF16(IDS_PICKER_CHANGE_TONE_MENU_LABEL))
.SetLeadingIcon(ui::ImageModel::FromVectorIcon(
chromeos::kEditorMenuEmojifyIcon, cros_tokens::kCrosSysOnSurface))
.Build();
// Case transformation results are shown in a submenu.
auto case_transform_submenu =
views::Builder<PickerItemWithSubmenuView>()
.SetSubmenuController(submenu_controller_)
.SetText(l10n_util::GetStringUTF16(
IDS_PICKER_CHANGE_CAPITALIZATION_MENU_LABEL))
.SetLeadingIcon(ui::ImageModel::FromVectorIcon(
kPickerSentenceCaseIcon, cros_tokens::kCrosSysOnSurface))
.Build();
for (const PickerSearchResult& result : results) {
if (std::holds_alternative<PickerCapsLockResult>(result)) {
delegate_->SetCapsLockDisplayed(true);
switch (delegate_->GetCapsLockPosition()) {
case PickerCapsLockPosition::kTop:
AddResultToSection(result, primary_section_view_);
break;
case PickerCapsLockPosition::kMiddle:
// TODO(b/357987564): Find a better way to put CapsLock at the end of
// the suggested section and remove the delay timer.
add_caps_lock_delay_timer_.Start(
FROM_HERE, kCapsLockDisplayDelay,
base::BindOnce(&PickerZeroStateView::AddResultToSection,
weak_ptr_factory_.GetWeakPtr(), result,
primary_section_view_));
break;
case PickerCapsLockPosition::kBottom:
AddResultToSection(result,
GetOrCreateSectionView(PickerCategoryType::kMore));
break;
}
} else if (std::holds_alternative<PickerNewWindowResult>(result)) {
new_window_submenu->AddEntry(
result, base::BindRepeating(&PickerZeroStateView::OnResultSelected,
weak_ptr_factory_.GetWeakPtr(), result));
} else if (const auto* editor_data =
std::get_if<PickerEditorResult>(&result)) {
auto callback =
base::BindRepeating(&PickerZeroStateView::OnResultSelected,
weak_ptr_factory_.GetWeakPtr(), result);
switch (GetEditorSubmenu(editor_data->category)) {
case EditorSubmenu::kNone:
primary_section_view_->AddResult(
result, preview_controller_,
PickerSectionView::LocalFileResultStyle::kList,
std::move(callback));
break;
case EditorSubmenu::kLength:
length_submenu->AddEntry(result, std::move(callback));
break;
case EditorSubmenu::kTone:
tone_submenu->AddEntry(result, std::move(callback));
break;
}
} else if (std::holds_alternative<PickerCaseTransformResult>(result)) {
case_transform_submenu->AddEntry(
result, base::BindRepeating(&PickerZeroStateView::OnResultSelected,
weak_ptr_factory_.GetWeakPtr(), result));
} else {
AddResultToSection(result, primary_section_view_);
}
}
if (!new_window_submenu->IsEmpty()) {
primary_section_view_->AddItemWithSubmenu(std::move(new_window_submenu));
}
if (!length_submenu->IsEmpty()) {
primary_section_view_->AddItemWithSubmenu(std::move(length_submenu));
}
if (!tone_submenu->IsEmpty()) {
primary_section_view_->AddItemWithSubmenu(std::move(tone_submenu));
}
// Add the submenu for case transformation.
if (!case_transform_submenu->IsEmpty()) {
GetOrCreateSectionView(PickerCategoryType::kCaseTransformations)
->AddItemWithSubmenu(std::move(case_transform_submenu));
}
delegate_->RequestPseudoFocus(section_list_view_->GetTopItem());
delegate_->OnZeroStateViewHeightChanged();
}
BEGIN_METADATA(PickerZeroStateView)
END_METADATA
} // namespace ash