// Copyright 2024 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_section_view.h"
#include <memory>
#include <string>
#include <utility>
#include "ash/bubble/bubble_utils.h"
#include "ash/picker/picker_asset_fetcher.h"
#include "ash/picker/views/picker_async_preview_image_view.h"
#include "ash/picker/views/picker_gif_view.h"
#include "ash/picker/views/picker_icons.h"
#include "ash/picker/views/picker_image_item_grid_view.h"
#include "ash/picker/views/picker_image_item_view.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_container_view.h"
#include "ash/picker/views/picker_list_item_view.h"
#include "ash/picker/views/picker_shortcut_hint_view.h"
#include "ash/picker/views/picker_strings.h"
#include "ash/picker/views/picker_traversable_item_container.h"
#include "ash/public/cpp/picker/picker_category.h"
#include "ash/public/cpp/picker/picker_search_result.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "base/functional/overloaded.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "build/branding_buildflags.h"
#include "chromeos/components/editor_menu/public/cpp/icon.h"
#include "chromeos/ui/base/file_icon_util.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "components/url_formatter/url_formatter.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/text_constants.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.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 {
// Some of the icons we use do not have a default size, so we need to manually
// set it.
constexpr int kIconSize = 20;
// Icons for browsing history should be smaller than the normal icon size.
constexpr auto kBrowsingHistoryIconSize = gfx::Size(18, 18);
constexpr auto kSectionTitleMargins = gfx::Insets::VH(8, 16);
constexpr auto kSectionTitleTrailingLinkMargins =
gfx::Insets::TLBR(4, 8, 4, 16);
constexpr int kLocalFileGridItemSize = 140;
PickerCategory GetCategoryForEditorData(const PickerEditorResult& data) {
switch (data.mode) {
case PickerEditorResult::Mode::kWrite:
return PickerCategory::kEditorWrite;
case PickerEditorResult::Mode::kRewrite:
return PickerCategory::kEditorRewrite;
}
}
std::u16string GetLabelForNewWindowType(PickerNewWindowResult::Type type) {
switch (type) {
case PickerNewWindowResult::Type::kDoc:
return l10n_util::GetStringUTF16(IDS_PICKER_NEW_GOOGLE_DOC_MENU_LABEL);
case PickerNewWindowResult::Type::kSheet:
return l10n_util::GetStringUTF16(IDS_PICKER_NEW_GOOGLE_SHEET_MENU_LABEL);
case PickerNewWindowResult::Type::kSlide:
return l10n_util::GetStringUTF16(IDS_PICKER_NEW_GOOGLE_SLIDE_MENU_LABEL);
case PickerNewWindowResult::Type::kChrome:
return l10n_util::GetStringUTF16(IDS_PICKER_NEW_GOOGLE_CHROME_MENU_LABEL);
}
}
const gfx::VectorIcon& GetIconForNewWindowType(
PickerNewWindowResult::Type type) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
switch (type) {
case PickerNewWindowResult::Type::kDoc:
return vector_icons::kGoogleDocsIcon;
case PickerNewWindowResult::Type::kSheet:
return vector_icons::kGoogleSheetsIcon;
case PickerNewWindowResult::Type::kSlide:
return vector_icons::kGoogleSlidesIcon;
case PickerNewWindowResult::Type::kChrome:
return vector_icons::kProductRefreshIcon;
}
#else
return kPlaceholderAppIcon;
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
}
std::u16string GetLabelForCaseTransformType(
PickerCaseTransformResult::Type type) {
switch (type) {
case PickerCaseTransformResult::Type::kUpperCase:
return l10n_util::GetStringUTF16(IDS_PICKER_UPPER_CASE_MENU_LABEL);
case PickerCaseTransformResult::Type::kLowerCase:
return l10n_util::GetStringUTF16(IDS_PICKER_LOWER_CASE_MENU_LABEL);
case PickerCaseTransformResult::Type::kTitleCase:
return l10n_util::GetStringUTF16(IDS_PICKER_TITLE_CASE_MENU_LABEL);
}
}
const gfx::VectorIcon& GetIconForCaseTransformType(
PickerCaseTransformResult::Type type) {
switch (type) {
case PickerCaseTransformResult::Type::kUpperCase:
return kPickerUpperCaseIcon;
case PickerCaseTransformResult::Type::kLowerCase:
return kPickerLowerCaseIcon;
case PickerCaseTransformResult::Type::kTitleCase:
return kPickerTitleCaseIcon;
}
}
std::u16string FormatBrowsingHistoryUrl(const GURL& url) {
return url_formatter::FormatUrl(
url,
url_formatter::kFormatUrlOmitDefaults |
url_formatter::kFormatUrlOmitHTTPS |
url_formatter::kFormatUrlOmitTrivialSubdomains,
base::UnescapeRule::SPACES, nullptr, nullptr, nullptr);
}
std::optional<base::File::Info> ResolveFileInfo(const base::FilePath& path) {
base::File::Info info;
if (!base::GetFileInfo(path, &info)) {
return std::nullopt;
}
return info;
}
// This should align with `chromeos::clipboard_history::GetIconForDescriptor`.
const gfx::VectorIcon& GetIconForClipboardData(
const PickerClipboardResult& data) {
switch (data.display_format) {
case PickerClipboardResult::DisplayFormat::kText:
return chromeos::kTextIcon;
case PickerClipboardResult::DisplayFormat::kUrl:
return vector_icons::kLinkIcon;
case PickerClipboardResult::DisplayFormat::kImage:
return chromeos::kFiletypeImageIcon;
case PickerClipboardResult::DisplayFormat::kFile:
return data.file_count == 1 ? chromeos::GetIconForPath(base::FilePath(
base::UTF16ToUTF8(data.display_text)))
: vector_icons::kContentCopyIcon;
case PickerClipboardResult::DisplayFormat::kHtml:
NOTREACHED();
}
NOTREACHED();
}
} // namespace
PickerSectionView::PickerSectionView(
int section_width,
PickerAssetFetcher* asset_fetcher,
PickerSubmenuController* submenu_controller)
: section_width_(section_width),
asset_fetcher_(asset_fetcher),
submenu_controller_(submenu_controller) {
SetLayoutManager(std::make_unique<views::BoxLayout>())
->SetOrientation(views::LayoutOrientation::kVertical);
title_container_ =
AddChildView(views::Builder<views::BoxLayoutView>()
.SetOrientation(views::LayoutOrientation::kHorizontal)
.Build());
GetViewAccessibility().SetRole(ax::mojom::Role::kList);
}
PickerSectionView::~PickerSectionView() = default;
std::unique_ptr<PickerItemView> PickerSectionView::CreateItemFromResult(
const PickerSearchResult& result,
PickerPreviewBubbleController* preview_controller,
PickerAssetFetcher* asset_fetcher,
int available_width,
LocalFileResultStyle local_file_result_style,
SelectResultCallback select_result_callback) {
using ReturnType = std::unique_ptr<PickerItemView>;
return std::visit(
base::Overloaded{
[&](const PickerTextResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
item_view->SetPrimaryText(data.primary_text);
item_view->SetSecondaryText(data.secondary_text);
item_view->SetLeadingIcon(data.icon);
return item_view;
},
[&](const PickerSearchRequestResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
item_view->SetPrimaryText(data.primary_text);
item_view->SetSecondaryText(data.secondary_text);
item_view->SetLeadingIcon(data.icon);
return item_view;
},
[&](const PickerEmojiResult& data) -> ReturnType { NOTREACHED(); },
[&](const PickerClipboardResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
switch (data.display_format) {
case PickerClipboardResult::DisplayFormat::kFile:
case PickerClipboardResult::DisplayFormat::kText:
case PickerClipboardResult::DisplayFormat::kUrl:
item_view->SetPrimaryText(data.display_text);
break;
case PickerClipboardResult::DisplayFormat::kImage:
if (!data.display_image.has_value()) {
return nullptr;
}
item_view->SetPrimaryImage(*data.display_image,
available_width);
break;
case PickerClipboardResult::DisplayFormat::kHtml:
NOTREACHED();
}
item_view->SetLeadingIcon(ui::ImageModel::FromVectorIcon(
GetIconForClipboardData(data), cros_tokens::kCrosSysOnSurface,
kIconSize));
return item_view;
},
[&](const PickerBrowsingHistoryResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
std::u16string formatted_url = FormatBrowsingHistoryUrl(data.url);
item_view->SetPrimaryText(data.title.empty() ? formatted_url
: data.title);
item_view->SetSecondaryText(formatted_url);
item_view->SetLeadingIcon(data.icon, kBrowsingHistoryIconSize);
return item_view;
},
[&](const PickerLocalFileResult& data) -> ReturnType {
switch (local_file_result_style) {
case LocalFileResultStyle::kList: {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
item_view->SetPrimaryText(data.title);
// `base::Unretained` is safe because `asset_fetcher` outlives
// the return value.
item_view->SetPreview(
preview_controller,
base::BindOnce(ResolveFileInfo, data.file_path),
data.file_path,
base::BindRepeating(&PickerAssetFetcher::FetchFileThumbnail,
base::Unretained(asset_fetcher)),
/*update_icon=*/true);
return item_view;
}
case LocalFileResultStyle::kGrid: {
// `base::Unretained` is safe because `asset_fetcher` outlives
// the return value.
auto image_view = std::make_unique<PickerAsyncPreviewImageView>(
data.file_path,
gfx::Size(kLocalFileGridItemSize, kLocalFileGridItemSize),
base::BindRepeating(&PickerAssetFetcher::FetchFileThumbnail,
base::Unretained(asset_fetcher)));
return std::make_unique<PickerImageItemView>(
std::move(select_result_callback), std::move(image_view));
}
}
},
[&](const PickerDriveFileResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
item_view->SetPrimaryText(data.title);
// TODO: b/333609460 - Handle dark/light mode.
item_view->SetLeadingIcon(
ui::ImageModel::FromImageSkia(chromeos::GetIconForPath(
data.file_path, /*dark_background=*/false, kIconSize)));
// `base::Unretained` is safe because `asset_fetcher` outlives the
// return value.
item_view->SetPreview(
preview_controller,
base::BindOnce(ResolveFileInfo, data.file_path), data.file_path,
base::BindRepeating(&PickerAssetFetcher::FetchFileThumbnail,
base::Unretained(asset_fetcher)),
/*update_icon=*/false);
return item_view;
},
[&](const PickerCategoryResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
item_view->SetPrimaryText(GetLabelForPickerCategory(data.category));
item_view->SetLeadingIcon(GetIconForPickerCategory(data.category));
return item_view;
},
[&](const PickerEditorResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
if (data.category.has_value()) {
// Preset write or rewrite.
item_view->SetPrimaryText(data.display_name);
item_view->SetLeadingIcon(ui::ImageModel::FromVectorIcon(
chromeos::editor_menu::GetIconForPresetQueryCategory(
*data.category),
cros_tokens::kCrosSysOnSurface));
} else {
// Freeform write or rewrite.
const PickerCategory category = GetCategoryForEditorData(data);
item_view->SetPrimaryText(GetLabelForPickerCategory(category));
item_view->SetLeadingIcon(GetIconForPickerCategory(category));
}
return item_view;
},
[&](const PickerNewWindowResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
item_view->SetPrimaryText(GetLabelForNewWindowType(data.type));
item_view->SetLeadingIcon(ui::ImageModel::FromVectorIcon(
GetIconForNewWindowType(data.type),
cros_tokens::kCrosSysOnSurface));
return item_view;
},
[&](const PickerCapsLockResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
item_view->SetPrimaryText(l10n_util::GetStringUTF16(
data.enabled ? IDS_PICKER_CAPS_LOCK_ON_MENU_LABEL
: IDS_PICKER_CAPS_LOCK_OFF_MENU_LABEL));
item_view->SetLeadingIcon(ui::ImageModel::FromVectorIcon(
data.enabled ? kPickerCapsLockOnIcon : kPickerCapsLockOffIcon,
cros_tokens::kCrosSysOnSurface));
item_view->SetShortcutHintView(
std::make_unique<PickerShortcutHintView>(data.shortcut));
return item_view;
},
[&](const PickerCaseTransformResult& data) -> ReturnType {
auto item_view = std::make_unique<PickerListItemView>(
std::move(select_result_callback));
item_view->SetPrimaryText(GetLabelForCaseTransformType(data.type));
item_view->SetLeadingIcon(ui::ImageModel::FromVectorIcon(
GetIconForCaseTransformType(data.type),
cros_tokens::kCrosSysOnSurface));
return item_view;
},
},
result);
}
void PickerSectionView::AddTitleLabel(const std::u16string& title_text) {
if (title_text.empty()) {
return;
}
title_label_ = title_container_->AddChildView(
views::Builder<views::Label>(
bubble_utils::CreateLabel(TypographyToken::kCrosAnnotation2,
title_text,
cros_tokens::kCrosSysOnSurfaceVariant))
.SetHorizontalAlignment(gfx::ALIGN_LEFT)
.SetProperty(views::kMarginsKey, kSectionTitleMargins)
.Build());
title_label_->GetViewAccessibility().SetRole(ax::mojom::Role::kHeading);
title_container_->SetFlexForView(title_label_, 1);
}
void PickerSectionView::AddTitleTrailingLink(
const std::u16string& link_text,
const std::u16string& accessible_name,
views::Link::ClickedCallback link_callback) {
title_trailing_link_ = title_container_->AddChildView(
views::Builder<views::Link>()
.SetText(link_text)
.SetCallback(link_callback)
.SetFontList(ash::TypographyProvider::Get()->ResolveTypographyToken(
ash::TypographyToken::kCrosAnnotation2))
.SetEnabledColorId(cros_tokens::kCrosSysPrimary)
.SetForceUnderline(false)
.SetProperty(views::kMarginsKey, kSectionTitleTrailingLinkMargins)
.Build());
title_trailing_link_->GetViewAccessibility().SetProperties(
ax::mojom::Role::kButton, accessible_name);
}
PickerListItemView* PickerSectionView::AddListItem(
std::unique_ptr<PickerListItemView> list_item) {
if (list_item_container_ == nullptr) {
list_item_container_ =
AddChildView(std::make_unique<PickerListItemContainerView>());
}
list_item->SetSubmenuController(submenu_controller_);
PickerListItemView* list_item_ptr =
list_item_container_->AddListItem(std::move(list_item));
item_views_.push_back(list_item_ptr);
return list_item_ptr;
}
PickerImageItemView* PickerSectionView::AddImageGridItem(
std::unique_ptr<PickerImageItemView> image_item) {
if (image_item_grid_ == nullptr) {
image_item_grid_ =
AddChildView(std::make_unique<PickerImageItemGridView>(section_width_));
}
image_item->SetSubmenuController(submenu_controller_);
PickerImageItemView* image_item_ptr =
image_item_grid_->AddImageItem(std::move(image_item));
item_views_.push_back(image_item_ptr);
return image_item_ptr;
}
PickerItemWithSubmenuView* PickerSectionView::AddItemWithSubmenu(
std::unique_ptr<PickerItemWithSubmenuView> item_with_submenu) {
if (list_item_container_ == nullptr) {
list_item_container_ =
AddChildView(std::make_unique<PickerListItemContainerView>());
}
PickerItemWithSubmenuView* item_ptr =
list_item_container_->AddItemWithSubmenu(std::move(item_with_submenu));
item_views_.push_back(item_ptr);
return item_ptr;
}
PickerItemView* PickerSectionView::AddResult(
const PickerSearchResult& result,
PickerPreviewBubbleController* preview_controller,
LocalFileResultStyle local_file_result_style,
SelectResultCallback select_result_callback) {
auto item = CreateItemFromResult(result, preview_controller, asset_fetcher_,
section_width_, local_file_result_style,
std::move(select_result_callback));
if (views::IsViewClass<PickerListItemView>(item.get())) {
return AddListItem(std::unique_ptr<PickerListItemView>(
views::AsViewClass<PickerListItemView>(item.release())));
}
if (views::IsViewClass<PickerImageItemView>(item.get())) {
return AddImageGridItem(std::unique_ptr<PickerImageItemView>(
views::AsViewClass<PickerImageItemView>(item.release())));
}
if (views::IsViewClass<PickerItemWithSubmenuView>(item.get())) {
return AddItemWithSubmenu(std::unique_ptr<PickerItemWithSubmenuView>(
views::AsViewClass<PickerItemWithSubmenuView>(item.release())));
}
NOTREACHED();
}
void PickerSectionView::ClearItems() {
item_views_.clear();
if (image_item_grid_ != nullptr) {
RemoveChildViewT(image_item_grid_.ExtractAsDangling());
}
if (list_item_container_ != nullptr) {
RemoveChildViewT(list_item_container_.ExtractAsDangling());
}
}
views::View* PickerSectionView::GetTopItem() {
return GetItemContainer() != nullptr ? GetItemContainer()->GetTopItem()
: nullptr;
}
views::View* PickerSectionView::GetBottomItem() {
return GetItemContainer() != nullptr ? GetItemContainer()->GetBottomItem()
: nullptr;
}
views::View* PickerSectionView::GetItemAbove(views::View* item) {
return GetItemContainer() != nullptr ? GetItemContainer()->GetItemAbove(item)
: nullptr;
}
views::View* PickerSectionView::GetItemBelow(views::View* item) {
return GetItemContainer() != nullptr ? GetItemContainer()->GetItemBelow(item)
: nullptr;
}
views::View* PickerSectionView::GetItemLeftOf(views::View* item) {
return GetItemContainer() != nullptr ? GetItemContainer()->GetItemLeftOf(item)
: nullptr;
}
views::View* PickerSectionView::GetItemRightOf(views::View* item) {
return GetItemContainer() != nullptr
? GetItemContainer()->GetItemRightOf(item)
: nullptr;
}
PickerTraversableItemContainer* PickerSectionView::GetItemContainer() {
if (image_item_grid_ != nullptr) {
return image_item_grid_;
}
return list_item_container_;
}
BEGIN_METADATA(PickerSectionView)
END_METADATA
} // namespace ash