// 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/system/mahi/summary_outlines_section.h"
#include <memory>
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "ash/system/mahi/mahi_animation_utils.h"
#include "ash/system/mahi/mahi_constants.h"
#include "ash/system/mahi/mahi_ui_update.h"
#include "ash/system/mahi/resources/grit/mahi_resources.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/components/mahi/public/cpp/mahi_manager.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "chromeos/ui/vector_icons/vector_icons.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/gfx/text_constants.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/animated_image_view.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
constexpr int64_t kSectionHeaderChildSpacing = 4;
constexpr int64_t kSectionHeaderIconSize = 16;
constexpr gfx::Insets kSectionPadding = gfx::Insets(16);
constexpr int64_t kSectionChildSpacing = 8;
constexpr int kTextLabelDefaultMaximumWidth =
mahi_constants::kScrollViewWidth - kSectionPadding.width();
std::unique_ptr<views::View> CreateSectionHeader(const gfx::VectorIcon& icon,
int name_id) {
auto view = std::make_unique<views::BoxLayoutView>();
view->SetOrientation(views::BoxLayout::Orientation::kHorizontal);
view->SetBetweenChildSpacing(kSectionHeaderChildSpacing);
view->AddChildView(
std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
icon, cros_tokens::kCrosSysOnSurface, kSectionHeaderIconSize)));
auto label =
std::make_unique<views::Label>(l10n_util::GetStringUTF16(name_id));
label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton2, *label);
view->AddChildView(std::move(label));
// TODO(b/330643995): Show the section header once other sections are
// available.
view->SetVisible(false);
return view;
}
} // namespace
SummaryOutlinesSection::SummaryOutlinesSection(MahiUiController* ui_controller)
: MahiUiController::Delegate(ui_controller), ui_controller_(ui_controller) {
CHECK(ui_controller_);
SetOrientation(views::BoxLayout::Orientation::kVertical);
SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kStart);
SetInsideBorderInsets(kSectionPadding);
SetBetweenChildSpacing(kSectionChildSpacing);
AddChildView(CreateSectionHeader(chromeos::kMahiSummarizeIcon,
IDS_MAHI_PANEL_SUMMARY_SECTION_NAME));
AddChildView(
views::Builder<views::AnimatedImageView>()
.CopyAddressTo(&summary_loading_animated_image_)
.SetID(mahi_constants::ViewId::kSummaryLoadingAnimatedImage)
.SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ASH_MAHI_LOADING_ACCESSIBLE_NAME))
.SetAnimatedImage(mahi_animation_utils::GetLottieAnimationData(
IDR_MAHI_LOADING_SUMMARY_ANIMATION))
.Build());
AddChildView(
views::Builder<views::Label>()
.CopyAddressTo(&summary_label_)
.SetVisible(false)
.SetID(mahi_constants::ViewId::kSummaryLabel)
.SetSelectable(true)
.SetMultiLine(true)
// TODO(crbug.com/40233803): Multiline label right now doesn't
// work well with `FlexLayout`. The size constraint is not
// passed down from the views tree in the first round of layout,
// so we impose a maximum width constraint so that the first
// layout handle the width and height constraint correctly.
.SetMaximumWidth(kTextLabelDefaultMaximumWidth)
.SetEnabledColorId(cros_tokens::kCrosSysOnSurface)
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
.AfterBuild(base::BindOnce([](views::Label* self) {
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2,
*self);
}))
.Build());
// TODO(b/330643995): Show the outlines section once it is ready.
auto* outlines_section_container = AddChildView(
views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kVertical)
.SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kStart)
.SetID(mahi_constants::ViewId::kOutlinesSectionContainer)
.SetVisible(false)
.Build());
outlines_section_container->AddChildView(CreateSectionHeader(
chromeos::kMahiOutlinesIcon, IDS_MAHI_PANEL_OUTLINES_SECTION_NAME));
outlines_section_container->AddChildView(
views::Builder<views::AnimatedImageView>()
.CopyAddressTo(&outlines_loading_animated_image_)
.SetID(mahi_constants::ViewId::kOutlinesLoadingAnimatedImage)
.SetAnimatedImage(mahi_animation_utils::GetLottieAnimationData(
IDR_MAHI_LOADING_OUTLINES_ANIMATION))
.SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ASH_MAHI_LOADING_ACCESSIBLE_NAME))
.Build());
outlines_section_container->AddChildView(
views::Builder<views::FlexLayoutView>()
.CopyAddressTo(&outlines_container_)
.SetID(mahi_constants::ViewId::kOutlinesContainer)
.SetOrientation(views::LayoutOrientation::kVertical)
.SetVisible(false)
.Build());
}
SummaryOutlinesSection::~SummaryOutlinesSection() = default;
views::View* SummaryOutlinesSection::GetView() {
return this;
}
bool SummaryOutlinesSection::GetViewVisibility(VisibilityState state) const {
switch (state) {
case VisibilityState::kError:
case VisibilityState::kQuestionAndAnswer:
return false;
case VisibilityState::kSummaryAndOutlines:
return true;
}
}
void SummaryOutlinesSection::OnUpdated(const MahiUiUpdate& update) {
switch (update.type()) {
case MahiUiUpdateType::kContentsRefreshInitiated:
case MahiUiUpdateType::kSummaryAndOutlinesReloaded:
LoadSummaryAndOutlines();
return;
case MahiUiUpdateType::kOutlinesLoaded:
HandleOutlinesLoaded(update.GetOutlines());
return;
case MahiUiUpdateType::kSummaryLoaded:
HandleSummaryLoaded(update.GetSummary());
return;
case MahiUiUpdateType::kAnswerLoaded:
case MahiUiUpdateType::kErrorReceived:
case MahiUiUpdateType::kQuestionAndAnswerViewNavigated:
case MahiUiUpdateType::kQuestionPosted:
case MahiUiUpdateType::kQuestionReAsked:
case MahiUiUpdateType::kRefreshAvailabilityUpdated:
case MahiUiUpdateType::kSummaryAndOutlinesSectionNavigated:
return;
}
}
void SummaryOutlinesSection::AddedToWidget() {
// When the view is first constructed, we need to wait until the view is added
// to the widget to announce loading state.
if (summary_loading_animated_image_->GetVisible() ||
outlines_loading_animated_image_->GetVisible()) {
GetViewAccessibility().AnnounceText(
l10n_util::GetStringUTF16(IDS_ASH_MAHI_LOADING_ACCESSIBLE_NAME));
}
}
void SummaryOutlinesSection::HandleOutlinesLoaded(
const std::vector<chromeos::MahiOutline>& outlines) {
outlines_container_->RemoveAllChildViews();
for (auto outline : outlines) {
outlines_container_->AddChildView(
views::Builder<views::Label>()
.SetText(outline.outline_content)
.SetSelectable(true)
.SetMultiLine(true)
.SetEnabledColorId(cros_tokens::kCrosSysOnSurface)
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
.AfterBuild(base::BindOnce([](views::Label* self) {
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2,
*self);
}))
.Build());
}
outlines_loading_animated_image_->Stop();
outlines_loading_animated_image_->SetVisible(false);
// TODO(b/330643995): Show the outlines section once it is ready. Note that
// when enabling outlines, we need to make sure that the state of fully
// loaded is announced in accessibility (similar to what is done in
// `HandleSummaryLoaded()`).
outlines_container_->SetVisible(false);
// TODO(b/333916944): Add metrics recording the outline loading animation time
// here.
}
void SummaryOutlinesSection::HandleSummaryLoaded(
const std::u16string& summary_text) {
summary_label_->SetVisible(true);
summary_label_->SetText(summary_text);
summary_loading_animated_image_->Stop();
summary_loading_animated_image_->SetVisible(false);
base::UmaHistogramTimes(mahi_constants::kSummaryLoadingTimeHistogramName,
base::Time::Now() - summary_start_loading_time_);
GetViewAccessibility().AnnounceText(
l10n_util::GetStringUTF16(IDS_ASH_MAHI_LOADED_ACCESSIBLE_NAME));
}
void SummaryOutlinesSection::LoadSummaryAndOutlines() {
if (!chromeos::MahiManager::Get()) {
CHECK_IS_TEST();
return;
}
if (summary_label_->GetVisible()) {
summary_label_->SetVisible(false);
summary_loading_animated_image_->SetVisible(true);
}
if (outlines_container_->GetVisible()) {
outlines_container_->SetVisible(false);
outlines_loading_animated_image_->SetVisible(true);
}
// Plays loading animation before summary and outlines are loaded.
summary_loading_animated_image_->Play(
mahi_animation_utils::GetLottiePlaybackConfig(
*summary_loading_animated_image_->animated_image()->skottie(),
IDR_MAHI_LOADING_SUMMARY_ANIMATION));
outlines_loading_animated_image_->Play(
mahi_animation_utils::GetLottiePlaybackConfig(
*outlines_loading_animated_image_->animated_image()->skottie(),
IDR_MAHI_LOADING_OUTLINES_ANIMATION));
GetViewAccessibility().AnnounceText(
l10n_util::GetStringUTF16(IDS_ASH_MAHI_LOADING_ACCESSIBLE_NAME));
summary_start_loading_time_ = base::Time::Now();
ui_controller_->UpdateSummaryAndOutlines();
}
BEGIN_METADATA(SummaryOutlinesSection)
END_METADATA
} // namespace ash