#include "ash/system/mahi/mahi_question_answer_view.h"
#include <memory>
#include <string>
#include <utility>
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/public/cpp/style/color_provider.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/icon_button.h"
#include "ash/style/system_textfield.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_controller.h"
#include "ash/system/mahi/mahi_ui_update.h"
#include "ash/system/mahi/mahi_utils.h"
#include "ash/system/mahi/resources/grit/mahi_resources.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/components/mahi/public/cpp/mahi_manager.h"
#include "chromeos/constants/chromeos_features.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/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/animated_image_view.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/view.h"
namespace ash {
namespace {
constexpr auto kErrorBubbleInteriorMargin = gfx::Insets::TLBR(4,
4,
0,
8);
constexpr int kErrorIconSize = 16;
constexpr auto kErrorLabelInteriorMargin =
gfx::Insets::TLBR(0, 8, 0, 0);
constexpr int kErrorLabelMaximumWidth = 276;
constexpr gfx::Insets kQuestionAnswerInteriorMargin(8);
constexpr auto kTextBubbleInteriorMargin =
gfx::Insets::VH(8, 12);
constexpr int kBetweenChildSpacing = 8;
constexpr int kTextBubbleCornerRadius = 12;
constexpr int kTextBubbleLabelDefaultMaximumWidth =
mahi_constants::kScrollViewDefaultWidth -
kQuestionAnswerInteriorMargin.width() - kTextBubbleInteriorMargin.width();
class ErrorBubble : public views::FlexLayoutView {
METADATA_HEADER(ErrorBubble, views::FlexLayoutView)
public:
explicit ErrorBubble(int error_text_id) {
views::Builder<views::FlexLayoutView>(this)
.SetBorder(views::CreateEmptyBorder(kErrorBubbleInteriorMargin))
.SetOrientation(views::LayoutOrientation::kHorizontal)
.AddChildren(
views::Builder<views::ImageView>()
.SetID(mahi_constants::ViewId::kQuestionAnswerErrorImage)
.SetImage(ui::ImageModel::FromVectorIcon(
vector_icons::kErrorIcon, cros_tokens::kCrosSysSecondary,
kErrorIconSize)),
views::Builder<views::Label>()
.SetBorder(views::CreateEmptyBorder(kErrorLabelInteriorMargin))
.SetFontList(TypographyProvider::Get()->ResolveTypographyToken(
TypographyToken::kCrosAnnotation1))
.SetID(mahi_constants::ViewId::kQuestionAnswerErrorLabel)
.SetMultiLine(true)
.SetMaximumWidth(kErrorLabelMaximumWidth)
.SetText(l10n_util::GetStringUTF16(error_text_id)))
.BuildChildren();
}
};
BEGIN_VIEW_BUILDER(ASH_EXPORT, ErrorBubble, views::FlexLayoutView)
END_VIEW_BUILDER
BEGIN_METADATA(ErrorBubble)
END_METADATA
views::Builder<views::FlexLayoutView> CreateTextBubbleBuilder(
const std::u16string& text,
bool is_question) {
return views::Builder<views::FlexLayoutView>()
.SetInteriorMargin(kTextBubbleInteriorMargin)
.SetBackground(views::CreateRoundedRectBackground(
is_question ? cros_tokens::kCrosSysSystemPrimaryContainer
: cros_tokens::kCrosSysSystemOnBase,
gfx::RoundedCornersF(kTextBubbleCornerRadius)))
.SetMainAxisAlignment(is_question ? views::LayoutAlignment::kEnd
: views::LayoutAlignment::kStart)
.CustomConfigure(base::BindOnce([](views::FlexLayoutView* layout) {
layout->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kPreferred,
true));
}))
.AddChildren(
views::Builder<views::Label>()
.SetID(mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel)
.SetSelectable(true)
.SetMultiLine(true)
.CustomConfigure(base::BindOnce([](views::Label* label) {
label->SetProperty(views::kFlexBehaviorKey,
views::FlexSpecification(
views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kPreferred,
true));
label->SetMaximumWidth(kTextBubbleLabelDefaultMaximumWidth);
}))
.SetText(text)
.SetTooltipText(text)
.SetHorizontalAlignment(is_question ? gfx::ALIGN_RIGHT
: gfx::ALIGN_LEFT)
.SetEnabledColor(
is_question ? cros_tokens::kCrosSysSystemOnPrimaryContainer
: cros_tokens::kCrosSysOnSurface)
.SetAutoColorReadabilityEnabled(false)
.SetSubpixelRenderingEnabled(false)
.SetFontList(TypographyProvider::Get()->ResolveTypographyToken(
TypographyToken::kCrosBody2)));
}
std::unique_ptr<views::View> CreateQuestionAnswerRow(const std::u16string& text,
bool is_question) {
views::Builder<views::FlexLayoutView> row_builder =
views::Builder<views::FlexLayoutView>()
.SetOrientation(views::LayoutOrientation::kHorizontal)
.CustomConfigure(base::BindOnce([](views::FlexLayoutView* layout) {
layout->SetLayoutManagerUseConstrainedSpace(
!base::FeatureList::IsEnabled(
chromeos::features::kMahiPanelResizable));
}));
views::Builder<views::FlexLayoutView> spacer =
views::Builder<views::FlexLayoutView>().CustomConfigure(
base::BindOnce([](views::FlexLayoutView* layout) {
layout->SetProperty(views::kFlexBehaviorKey,
views::FlexSpecification(
views::LayoutOrientation::kHorizontal,
views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kUnbounded,
true));
}));
if (is_question) {
return std::move(row_builder)
.AddChildren(spacer, CreateTextBubbleBuilder(text, is_question))
.Build();
}
return std::move(row_builder)
.AddChildren(CreateTextBubbleBuilder(text, is_question), spacer)
.Build();
}
}
}
DEFINE_VIEW_BUILDER(ASH_EXPORT, ash::ErrorBubble)
namespace ash {
MahiQuestionAnswerView::QuestionCountReporter::QuestionCountReporter() =
default;
MahiQuestionAnswerView::QuestionCountReporter::~QuestionCountReporter() =
default;
void MahiQuestionAnswerView::QuestionCountReporter::IncreaseQuestionCount() {
++question_count_;
}
void MahiQuestionAnswerView::QuestionCountReporter::ReportDataAndReset() {
base::UmaHistogramCounts100(
mahi_constants::kQuestionCountPerMahiSessionHistogramName,
question_count_);
question_count_ = 0;
}
MahiQuestionAnswerView::MahiQuestionAnswerView(MahiUiController* ui_controller)
: MahiUiController::Delegate(ui_controller), ui_controller_(ui_controller) {
CHECK(ui_controller);
SetOrientation(views::LayoutOrientation::kVertical);
SetInteriorMargin(kQuestionAnswerInteriorMargin);
SetIgnoreDefaultMainAxisMargins(true);
SetCollapseMargins(true);
SetDefault(views::kMarginsKey, gfx::Insets::VH(kBetweenChildSpacing, 0));
SetLayoutManagerUseConstrainedSpace(
!base::FeatureList::IsEnabled(chromeos::features::kMahiPanelResizable));
}
MahiQuestionAnswerView::~MahiQuestionAnswerView() {
question_count_reporter_.ReportDataAndReset();
}
views::View* MahiQuestionAnswerView::GetView() {
return this;
}
bool MahiQuestionAnswerView::GetViewVisibility(VisibilityState state) const {
switch (state) {
case VisibilityState::kQuestionAndAnswer:
return true;
case VisibilityState::kError:
case VisibilityState::kSummaryAndOutlinesAndElucidation:
return false;
}
}
void MahiQuestionAnswerView::OnUpdated(const MahiUiUpdate& update) {
switch (update.type()) {
case MahiUiUpdateType::kAnswerLoaded: {
RemoveLoadingAnimatedImage();
base::UmaHistogramTimes(
mahi_constants::kAnswerLoadingTimeHistogramName,
base::TimeTicks::Now() - answer_start_loading_time_);
auto& answer = update.GetAnswer();
AddChildView(CreateQuestionAnswerRow(answer, false));
GetViewAccessibility().AnnounceText(answer);
return;
}
case MahiUiUpdateType::kContentsRefreshInitiated:
question_count_reporter_.ReportDataAndReset();
RemoveAllChildViews();
return;
case MahiUiUpdateType::kErrorReceived: {
RemoveLoadingAnimatedImage();
const MahiUiError& error = update.GetError();
if (error.origin_state == VisibilityState::kQuestionAndAnswer) {
AddChildView(
views::Builder<ErrorBubble>(
std::make_unique<ErrorBubble>(
mahi_utils::GetErrorStatusViewTextId(error.status)))
.Build());
}
return;
}
case MahiUiUpdateType::kQuestionPosted: {
question_count_reporter_.IncreaseQuestionCount();
AddChildView(CreateQuestionAnswerRow(update.GetQuestion(),
true));
if (answer_loading_animated_image_) {
LOG(ERROR) << "Loading animated image shouldn't be running when a "
"question can be asked";
return;
}
auto* answer_loading_animated_image = AddChildView(
views::Builder<views::AnimatedImageView>()
.SetID(mahi_constants::ViewId::kAnswerLoadingAnimatedImage)
.SetAccessibleName(l10n_util::GetStringUTF16(
IDS_ASH_MAHI_LOADING_ACCESSIBLE_NAME))
.SetAnimatedImage(mahi_animation_utils::GetLottieAnimationData(
IDR_MAHI_LOADING_SUMMARY_ANIMATION))
.SetHorizontalAlignment(views::ImageViewBase::Alignment::kLeading)
.AfterBuild(base::BindOnce([](views::AnimatedImageView* self) {
self->Play(mahi_animation_utils::GetLottiePlaybackConfig(
*self->animated_image()->skottie(),
IDR_MAHI_LOADING_SUMMARY_ANIMATION));
}))
.Build());
answer_loading_animated_image_.SetView(answer_loading_animated_image);
answer_start_loading_time_ = base::TimeTicks::Now();
return;
}
case MahiUiUpdateType::kQuestionReAsked: {
const MahiQuestionParams& question_params =
update.GetReAskQuestionParams();
ui_controller_->SendQuestion(question_params.question,
question_params.current_panel_content,
MahiUiController::QuestionSource::kRetry);
return;
}
case MahiUiUpdateType::kOutlinesLoaded:
case MahiUiUpdateType::kPanelBoundsChanged:
case MahiUiUpdateType::kQuestionAndAnswerViewNavigated:
case MahiUiUpdateType::kRefreshAvailabilityUpdated:
case MahiUiUpdateType::kSummaryLoaded:
case MahiUiUpdateType::kSummaryAndOutlinesSectionNavigated:
case MahiUiUpdateType::kSummaryAndOutlinesReloaded:
case MahiUiUpdateType::kElucidationRequested:
case MahiUiUpdateType::kElucidationLoaded:
return;
}
}
void MahiQuestionAnswerView::RemoveLoadingAnimatedImage() {
if (answer_loading_animated_image_) {
RemoveChildViewT(answer_loading_animated_image_.view());
}
}
BEGIN_METADATA(MahiQuestionAnswerView)
END_METADATA
}