// Copyright 2022 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/system/phonehub/app_stream_launcher_view.h"
#include <cmath>
#include <memory>
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/controls/rounded_scroll_bar.h"
#include "ash/resources/vector_icons/vector_icons.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/style_util.h"
#include "ash/style/typography.h"
#include "ash/system/phonehub/app_stream_launcher_item.h"
#include "ash/system/phonehub/app_stream_launcher_list_item.h"
#include "ash/system/phonehub/app_stream_launcher_view.h"
#include "ash/system/phonehub/phone_hub_view_ids.h"
#include "ash/system/phonehub/ui_constants.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
#include "chromeos/ash/components/phonehub/notification.h"
#include "chromeos/ash/components/phonehub/phone_hub_manager.h"
#include "chromeos/ash/components/phonehub/user_action_recorder.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/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/view.h"
namespace ash {
namespace {
// Insets for the vertical scroll bar.
constexpr auto kVerticalScrollInsets = gfx::Insets::TLBR(1, 0, 1, 1);
constexpr auto kHeaderDefaultSpacing = gfx::Insets::VH(0, 0);
constexpr gfx::Size kDefaultAppListScrollViewSize = gfx::Size(400, 400);
// The horizontal interior margin for the apps page container - i.e. the margin
// between the apps page bounds and the page content.
constexpr int kHorizontalInteriorMargin = 25;
// Number of columns of apps in the grid
constexpr int kColumns = 4;
constexpr int kRowHeight = 70;
constexpr auto kHeaderViewInsets = gfx::Insets::TLBR(25, 15, 15, 15);
constexpr int kAppViewWidth = 50;
constexpr int kHeaderChildrenSpacing = 20;
// The padding between different sections within the apps page. Also used for
// interior apps page container margin.
constexpr int kVerticalPaddingBetweenSections = 16;
constexpr int kAppListItemHorizontalMargin = 16;
constexpr int kAppListItemSpacing = 8;
} // namespace
AppStreamLauncherView::AppStreamLauncherView(
phonehub::PhoneHubManager* phone_hub_manager)
: phone_hub_manager_(phone_hub_manager) {
SetID(PhoneHubViewID::kAppStreamLauncherView);
auto* layout_manager =
SetLayoutManager(std::make_unique<views::FlexLayout>());
layout_manager->SetInteriorMargin(gfx::Insets::VH(0, 0))
.SetOrientation(views::LayoutOrientation::kVertical)
.SetCollapseMargins(false)
.SetDefault(views::kMarginsKey, kHeaderDefaultSpacing)
.SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
AddChildView(CreateHeaderView());
auto* app_list_view = AddChildView(CreateAppListView());
gfx::Size launcher_size;
if (phone_hub_manager->GetAppStreamLauncherDataModel() &&
phone_hub_manager->GetAppStreamLauncherDataModel()->launcher_height() >
kDefaultAppListScrollViewSize.height()) {
launcher_size = gfx::Size(
phone_hub_manager->GetAppStreamLauncherDataModel()->launcher_width(),
phone_hub_manager->GetAppStreamLauncherDataModel()->launcher_height());
} else {
launcher_size = kDefaultAppListScrollViewSize;
}
app_list_view->SetPreferredSize(launcher_size);
app_list_view->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
views::MaximumFlexSizeRule::kPreferred,
/*adjust_height_for_width =*/false)
.WithWeight(1));
phone_hub_manager->GetUserActionRecorder()->RecordUiOpened();
UpdateFromDataModel();
if (phone_hub_manager->GetAppStreamLauncherDataModel())
phone_hub_manager->GetAppStreamLauncherDataModel()->AddObserver(this);
}
AppStreamLauncherView::~AppStreamLauncherView() {
if (phone_hub_manager_->GetAppStreamLauncherDataModel())
phone_hub_manager_->GetAppStreamLauncherDataModel()->RemoveObserver(this);
}
// The behavior is inspired from ash/app_list/views/app_list_bubble_apps_page.cc
std::unique_ptr<views::View> AppStreamLauncherView::CreateAppListView() {
// The entire page scrolls.
auto scroll_view = std::make_unique<views::ScrollView>(
views::ScrollView::ScrollWithLayers::kEnabled);
scroll_view->ClipHeightTo(0, std::numeric_limits<int>::max());
scroll_view->SetDrawOverflowIndicator(false);
// Don't paint a background. The bubble already has one.
scroll_view->SetBackgroundColor(std::nullopt);
// Arrow keys are used to select app icons.
scroll_view->SetAllowKeyboardScrolling(false);
// Scroll view will have a gradient mask layer.
scroll_view->SetPaintToLayer(ui::LAYER_NOT_DRAWN);
// Set up scroll bars.
scroll_view->SetHorizontalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
auto vertical_scroll = std::make_unique<RoundedScrollBar>(
views::ScrollBar::Orientation::kVertical);
vertical_scroll->SetInsets(kVerticalScrollInsets);
vertical_scroll->SetSnapBackOnDragOutside(false);
scroll_view->SetVerticalScrollBar(std::move(vertical_scroll));
auto scroll_contents = std::make_unique<views::View>();
scroll_contents->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
views::MaximumFlexSizeRule::kUnbounded,
/*adjust_height_for_width =*/false)
.WithWeight(1));
auto* layout =
scroll_contents->SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetOrientation(views::LayoutOrientation::kVertical)
.SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
if (features::IsEcheLauncherListViewEnabled()) {
layout->SetInteriorMargin(gfx::Insets::VH(kVerticalPaddingBetweenSections,
kAppListItemHorizontalMargin));
} else {
layout->SetInteriorMargin(gfx::Insets::VH(kVerticalPaddingBetweenSections,
kHorizontalInteriorMargin));
}
// All apps section.
items_container_ =
scroll_contents->AddChildView(std::make_unique<views::View>());
items_container_->SetPaintToLayer();
items_container_->layer()->SetFillsBoundsOpaquely(false);
scroll_view->SetContents(std::move(scroll_contents));
return scroll_view;
}
void AppStreamLauncherView::AppIconActivated(
phonehub::Notification::AppMetadata app) {
auto* interaction_handler_ =
phone_hub_manager_->GetRecentAppsInteractionHandler();
if (!interaction_handler_)
return;
interaction_handler_->NotifyRecentAppClicked(
app, eche_app::mojom::AppStreamLaunchEntryPoint::APPS_LIST);
}
void AppStreamLauncherView::UpdateFromDataModel() {
if (!items_container_)
return;
items_container_->RemoveAllChildViews();
if (!phone_hub_manager_->GetAppStreamLauncherDataModel())
return;
const std::vector<phonehub::Notification::AppMetadata>* apps_list =
phone_hub_manager_->GetAppStreamLauncherDataModel()
->GetAppsListSortedByName();
if (features::IsEcheLauncherListViewEnabled()) {
CreateListView(apps_list);
} else {
CreateGridView(apps_list);
}
}
std::unique_ptr<views::View> AppStreamLauncherView::CreateItemView(
const phonehub::Notification::AppMetadata& app) {
return std::make_unique<AppStreamLauncherItem>(
base::BindRepeating(&AppStreamLauncherView::AppIconActivated,
base::Unretained(this), app),
app);
}
std::unique_ptr<views::View> AppStreamLauncherView::CreateListItemView(
const phonehub::Notification::AppMetadata& app) {
return std::make_unique<AppStreamLauncherListItem>(
base::BindRepeating(&AppStreamLauncherView::AppIconActivated,
base::Unretained(this), app),
app);
}
std::unique_ptr<views::View> AppStreamLauncherView::CreateHeaderView() {
auto header = std::make_unique<views::View>();
header->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, kHeaderViewInsets,
kHeaderChildrenSpacing));
header->SetBackground(views::CreateThemedSolidBackground(
kColorAshControlBackgroundColorInactive));
// Add arrowback button
arrow_back_button_ = header->AddChildView(CreateButton(
base::BindRepeating(&AppStreamLauncherView::OnArrowBackActivated,
weak_factory_.GetWeakPtr()),
kEcheArrowBackIcon, IDS_APP_ACCNAME_BACK));
views::Label* title = header->AddChildView(std::make_unique<views::Label>(
std::u16string(), views::style::CONTEXT_DIALOG_TITLE,
views::style::STYLE_PRIMARY,
gfx::DirectionalityMode::DIRECTIONALITY_AS_URL));
title->SetMultiLine(true);
title->SetAllowCharacterBreak(true);
title->SetProperty(views::kBoxLayoutFlexKey,
views::BoxLayoutFlexSpecification());
title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title->SetText(
l10n_util::GetStringUTF16(IDS_ASH_PHONE_HUB_APP_STREAM_LAUNCHER_TITLE));
TypographyProvider::Get()->StyleLabel(ash::TypographyToken::kCrosHeadline1,
*title);
return header;
}
// Creates a button with the given callback, icon, and tooltip text.
// `message_id` is the resource id of the tooltip text of the icon.
std::unique_ptr<views::Button> AppStreamLauncherView::CreateButton(
views::Button::PressedCallback callback,
const gfx::VectorIcon& icon,
int message_id) {
SkColor color = AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorPrimary);
SkColor disabled_color = SkColorSetA(color, gfx::kDisabledControlAlpha);
auto button = views::CreateVectorImageButton(std::move(callback));
views::SetImageFromVectorIconWithColor(button.get(), icon, color,
disabled_color);
ash::StyleUtil::SetUpInkDropForButton(button.get(), gfx::Insets(),
/*highlight_on_hover=*/false,
/*highlight_on_focus=*/true);
views::FocusRing::Get(button.get())
->SetColorId(static_cast<ui::ColorId>(cros_tokens::kCrosSysFocusRing));
button->SetTooltipText(l10n_util::GetStringUTF16(message_id));
button->SizeToPreferredSize();
views::InstallCircleHighlightPathGenerator(button.get());
return button;
}
void AppStreamLauncherView::OnArrowBackActivated() {
phone_hub_manager_->GetAppStreamLauncherDataModel()
->SetShouldShowMiniLauncher(false);
}
void AppStreamLauncherView::ChildPreferredSizeChanged(View* child) {
// Resize the bubble when the child change its size.
PreferredSizeChanged();
}
void AppStreamLauncherView::ChildVisibilityChanged(View* child) {
// Resize the bubble when the child change its visibility.
PreferredSizeChanged();
}
phone_hub_metrics::Screen AppStreamLauncherView::GetScreenForMetrics() const {
return phone_hub_metrics::Screen::kMiniLauncher;
}
void AppStreamLauncherView::OnBubbleClose() {
RemoveAllChildViews();
}
void AppStreamLauncherView::OnAppListChanged() {
if (!features::IsEcheSWAEnabled() || !features::IsEcheLauncherEnabled())
return;
UpdateFromDataModel();
}
void AppStreamLauncherView::CreateListView(
const std::vector<phonehub::Notification::AppMetadata>* apps_list) {
items_container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets::VH(0, 0),
kAppListItemSpacing));
for (auto& app : *apps_list) {
items_container_->AddChildView(CreateListItemView(app));
}
}
void AppStreamLauncherView::CreateGridView(
const std::vector<phonehub::Notification::AppMetadata>* apps_list) {
auto* table_layout = items_container_->SetLayoutManager(
std::make_unique<views::TableLayout>());
int spacing = (kTrayMenuWidth - kHorizontalInteriorMargin * 2 -
kAppViewWidth * kColumns) /
(kColumns - 1);
for (int i = 0; i < kColumns; i++) {
table_layout->AddColumn(
views::LayoutAlignment::kStretch, views::LayoutAlignment::kStretch, 1.0,
views::TableLayout::ColumnSize::kUsePreferred, 0, 0);
if (i != kColumns - 1) {
table_layout->AddPaddingColumn(1.0, spacing);
}
}
table_layout->AddRows(ceil((double)apps_list->size() / kColumns),
views::TableLayout::kFixedSize, kRowHeight);
for (auto& app : *apps_list) {
items_container_->AddChildView(CreateItemView(app));
}
}
BEGIN_METADATA(AppStreamLauncherView)
END_METADATA
} // namespace ash