chromium/chrome/browser/ui/quick_answers/ui/quick_answers_view.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/quick_answers/ui/quick_answers_view.h"

#include <string>
#include <string_view>

#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/chromeos/read_write_cards/read_write_cards_ui_controller.h"
#include "chrome/browser/ui/chromeos/read_write_cards/read_write_cards_view.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/quick_answers/quick_answers_ui_controller.h"
#include "chrome/browser/ui/quick_answers/ui/loading_view.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_stage_button.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_text_label.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_util.h"
#include "chrome/browser/ui/quick_answers/ui/result_view.h"
#include "chrome/browser/ui/quick_answers/ui/retry_view.h"
#include "chrome/browser/ui/quick_answers/ui/typography.h"
#include "chrome/browser/ui/views/editor_menu/utils/focus_search.h"
#include "chrome/browser/ui/views/editor_menu/utils/pre_target_handler.h"
#include "chromeos/components/magic_boost/public/cpp/views/experiment_badge.h"
#include "chromeos/components/quick_answers/public/cpp/constants.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/components/quick_answers/utils/quick_answers_metrics.h"
#include "chromeos/components/quick_answers/utils/quick_answers_utils.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/ui_base_types.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/display/screen.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/font.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/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/label_button.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/menu/menu_config.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/fill_layout.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_provider.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/metadata/view_factory_internal.h"
#include "ui/views/painter.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace quick_answers {
namespace {

using quick_answers::PhoneticsInfo;
using quick_answers::QuickAnswer;
using quick_answers::QuickAnswerResultText;
using quick_answers::QuickAnswerText;
using quick_answers::QuickAnswerUiElement;
using quick_answers::QuickAnswerUiElementType;
using quick_answers::ResultType;
using views::Button;
using views::Label;
using views::View;

constexpr auto kMainViewInsets = gfx::Insets::TLBR(12, 8, 12, 16);
constexpr auto kContentViewInsets = gfx::Insets::TLBR(0, 8, 0, 0);

// Google icon.
constexpr int kIconSizeDip = 16;

// Spacing between lines in the main view.
constexpr int kLineSpacingDip = 4;
constexpr int kDefaultLineHeightDip = 20;

// Buttons view.
constexpr int kButtonsViewMarginDip = 4;
constexpr int kButtonsSpacingDip = 4;
constexpr int kDogfoodButtonSizeDip = 20;
constexpr int kSettingsButtonSizeDip = 14;
constexpr int kSettingsButtonBorderDip = 3;

const gfx::Insets GetMainViewInsets(Design design) {
  switch (design) {
    case Design::kCurrent:
      return kMainViewInsets;
    case Design::kRefresh:
    case Design::kMagicBoost:
      return gfx::Insets::TLBR(12, 16, 16, 16);
  }

  CHECK(false) << "Invalid design enum value provided";
}

const gfx::Insets GetIconInsets(Design design) {
  switch (design) {
    case Design::kCurrent:
      return gfx::Insets(views::LayoutProvider::Get()->GetInsetsMetric(
          views::InsetsMetric::INSETS_ICON_BUTTON));
    case Design::kRefresh:
    case Design::kMagicBoost:
      return gfx::Insets::TLBR(2, 0, 0, 0);
  }

  CHECK(false) << "Invalid design enum value provided";
}

const gfx::Insets GetButtonsViewInsets(Design design) {
  switch (design) {
    case Design::kCurrent:
      return gfx::Insets(kButtonsViewMarginDip);
    case Design::kRefresh:
    case Design::kMagicBoost:
      // Buttons view is rendered as a layer on top of main view. For `kRefresh`
      // and `kMagicBoost`, they share the same insets.
      return GetMainViewInsets(design);
  }

  CHECK(false) << "Invalid design enum value provided";
}

const gfx::VectorIcon& GetVectorIcon(std::optional<Intent> intent) {
  if (!intent) {
    return omnibox::kAnswerDefaultIcon;
  }

  switch (intent.value()) {
    case Intent::kDefinition:
      return omnibox::kAnswerDictionaryIcon;
    case Intent::kTranslation:
      return omnibox::kAnswerTranslationIcon;
    case Intent::kUnitConversion:
      return omnibox::kAnswerCalculatorIcon;
  }

  CHECK(false) << "Invalid intent enum value specified";
}

ui::ImageModel GetIcon(Design design, std::optional<Intent> intent) {
  switch (design) {
    case Design::kCurrent:
      return ui::ImageModel::FromVectorIcon(
          vector_icons::kGoogleColorIcon, gfx::kPlaceholderColor, kIconSizeDip);
    case Design::kRefresh:
      return ui::ImageModel::FromVectorIcon(
          GetVectorIcon(intent), ui::kColorSysOnSurface, kIconSizeDip);
    case Design::kMagicBoost:
      return ui::ImageModel::FromVectorIcon(chromeos::kInfoSparkIcon,
                                            ui::ColorIds::kColorSysOnSurface,
                                            kIconSizeDip);
  }

  CHECK(false) << "Invalid design enum value specified";
}

void SetResultTo(ResultView* result_view, DefinitionResult* definition_result) {
  if (definition_result->phonetics_info.text.empty()) {
    result_view->SetFirstLineText(base::UTF8ToUTF16(definition_result->word));
  } else {
    result_view->SetFirstLineText(base::UTF8ToUTF16(BuildDefinitionTitleText(
        definition_result->word, definition_result->phonetics_info.text)));
  }

  if (definition_result->phonetics_info.PhoneticsInfoAvailable()) {
    result_view->SetPhoneticsInfo(definition_result->phonetics_info);
  }

  result_view->SetSecondLineText(
      base::UTF8ToUTF16(definition_result->sense.definition));
}

void SetResultTo(ResultView* result_view,
                 TranslationResult* translation_result,
                 Design design) {
  result_view->SetFirstLineText(
      base::UTF8ToUTF16(translation_result->text_to_translate));

  if (design != Design::kCurrent) {
    std::u16string display_name_locale =
        l10n_util::GetDisplayNameForLocaleWithoutCountry(
            translation_result->source_locale,
            g_browser_process->GetApplicationLocale(), /*is_for_ui=*/true);
    if (!display_name_locale.empty()) {
      result_view->SetFirstLineSubText(display_name_locale);
    }
  }

  result_view->SetSecondLineText(
      base::UTF8ToUTF16(translation_result->translated_text));
}

void SetResultTo(ResultView* result_view,
                 UnitConversionResult* unit_conversion_result) {
  result_view->SetFirstLineText(
      base::UTF8ToUTF16(unit_conversion_result->source_text));
  result_view->SetSecondLineText(
      base::UTF8ToUTF16(unit_conversion_result->result_text));
}

void SetNoResult(ResultView* result_view, std::string_view title) {
  result_view->SetFirstLineText(base::UTF8ToUTF16(title));
  result_view->SetSecondLineText(
      l10n_util::GetStringUTF16(IDS_QUICK_ANSWERS_VIEW_NO_RESULT_V2));
}

std::u16string GetIntentName(std::optional<Intent> intent) {
  if (!intent) {
    // The original intent name is kept if the backend finds out that it cannot
    // find an intent in a query. It means that `std::nullopt` case is shown
    // only for Linux-ChromeOS.
    return std::u16string();
  }

  switch (intent.value()) {
    case Intent::kDefinition:
      return l10n_util::GetStringUTF16(
          IDS_QUICK_ANSWERS_RESULT_HEADER_INTENT_DEFINITION);
    case Intent::kTranslation:
      return l10n_util::GetStringUTF16(
          IDS_QUICK_ANSWERS_RESULT_HEADER_INTENT_TRANSLATION);
    case Intent::kUnitConversion:
      return l10n_util::GetStringUTF16(
          IDS_QUICK_ANSWERS_RESULT_HEADER_INTENT_UNIT_CONVERSION);
  }

  CHECK(false) << "Invalid intent enum value specified";
}

// TODO(b/340629098): A temporary solution until buttons view is merged into
// headers. See another comment for buttons view in
// `QuickAnswersView::QuickAnswersView` about details.
int GetButtonsViewOcclusion(Design design) {
  gfx::Insets insets_icon_button =
      views::LayoutProvider::Get()->GetInsetsMetric(
          views::InsetsMetric::INSETS_ICON_BUTTON);
  return insets_icon_button.left() + kIconSizeDip + insets_icon_button.right() +
         GetButtonsViewInsets(design).right();
}

views::Builder<views::Label> GetRefreshUiHeader() {
  int line_height = GetCrosAnnotation1LineHeight();
  int vertical_padding = std::max(0, (20 - line_height) / 2);

  return views::Builder<views::Label>()
      .SetFontList(GetCrosAnnotation1FontList().DeriveWithWeight(
          gfx::Font::Weight::MEDIUM))
      .SetLineHeight(line_height)
      .SetProperty(
          views::kMarginsKey,
          gfx::Insets::TLBR(
              vertical_padding, 0,
              vertical_padding +
                  views::LayoutProvider::Get()->GetDistanceMetric(
                      views::DistanceMetric::DISTANCE_RELATED_CONTROL_VERTICAL),
              GetButtonsViewOcclusion(Design::kRefresh)))
      .SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
      .SetProperty(
          views::kFlexBehaviorKey,
          views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
                                   views::MaximumFlexSizeRule::kPreferred));
}

views::Builder<views::BoxLayoutView> GetMagicBoostHeader() {
  int line_height = GetCrosAnnotation1LineHeight();
  int vertical_padding = std::max(0, (20 - line_height) / 2);

  return views::Builder<views::BoxLayoutView>()
      .SetProperty(
          views::kMarginsKey,
          gfx::Insets::TLBR(
              0, 0,
              views::LayoutProvider::Get()->GetDistanceMetric(
                  views::DistanceMetric::DISTANCE_RELATED_CONTROL_VERTICAL),
              GetButtonsViewOcclusion(Design::kMagicBoost)))
      .SetOrientation(views::LayoutOrientation::kHorizontal)
      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
      .SetProperty(
          views::kFlexBehaviorKey,
          views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
                                   views::MaximumFlexSizeRule::kPreferred))
      .SetBetweenChildSpacing(views::LayoutProvider::Get()->GetDistanceMetric(
          views::DistanceMetric::DISTANCE_RELATED_BUTTON_HORIZONTAL))
      .AddChild(
          views::Builder<views::Label>()
              .SetText(l10n_util::GetStringUTF16(IDS_ASH_MAHI_MENU_TITLE))
              .SetLineHeight(line_height)
              .SetProperty(views::kMarginsKey,
                           gfx::Insets::VH(vertical_padding, 0))
              .SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
              .SetFontList(GetCrosAnnotation1FontList().DeriveWithWeight(
                  gfx::Font::Weight::MEDIUM)))
      .AddChild(views::Builder<chromeos::ExperimentBadge>());
}

}  // namespace

// QuickAnswersView -----------------------------------------------------------

QuickAnswersView::QuickAnswersView(
    const QuickAnswersView::Params& params,
    base::WeakPtr<QuickAnswersUiController> controller)
    : chromeos::ReadWriteCardsView(controller->GetReadWriteCardsUiController()),
      controller_(std::move(controller)),
      title_(params.title),
      design_(params.design),
      is_internal_(params.is_internal),
      focus_search_(std::make_unique<chromeos::editor_menu::FocusSearch>(
          this,
          base::BindRepeating(&QuickAnswersView::GetFocusableViews,
                              base::Unretained(this)))) {
  SetBackground(
      views::CreateThemedSolidBackground(ui::kColorPrimaryBackground));
  SetUseDefaultFillLayout(true);

  std::unique_ptr<views::FlexLayout> main_view_layout =
      std::make_unique<views::FlexLayout>();
  main_view_layout->SetOrientation(views::LayoutOrientation::kHorizontal)
      .SetInteriorMargin(GetMainViewInsets(design_))
      .SetCrossAxisAlignment(views::LayoutAlignment::kStart);

  AddChildView(
      views::Builder<QuickAnswersStageButton>()
          .CopyAddressTo(&quick_answers_stage_button_)
          .SetCallback(base::BindRepeating(
              &QuickAnswersView::SendQuickAnswersQuery, base::Unretained(this)))
          .SetAccessibleName(
              l10n_util::GetStringUTF16(IDS_QUICK_ANSWERS_VIEW_A11Y_NAME_TEXT))
          .SetLayoutManager(std::move(main_view_layout))
          .AddChild(views::Builder<views::ImageView>().CopyAddressTo(&icon_))
          .AddChild(
              views::Builder<views::FlexLayoutView>()
                  .SetInteriorMargin(kContentViewInsets)
                  .SetProperty(views::kFlexBehaviorKey,
                               views::FlexSpecification(
                                   views::MinimumFlexSizeRule::kScaleToZero,
                                   views::MaximumFlexSizeRule::kPreferred,
                                   /*adjust_height_for_width=*/true))
                  .SetOrientation(views::LayoutOrientation::kVertical)
                  .AddChild(GetRefreshUiHeader()
                                .SetVisible(design_ == Design::kRefresh)
                                .CopyAddressTo(&refreshed_ui_header_))
                  .AddChild(GetMagicBoostHeader().SetVisible(
                      design_ == Design::kMagicBoost))
                  .AddChild(
                      views::Builder<LoadingView>()
                          .CopyAddressTo(&loading_view_)
                          .SetFirstLineText(base::UTF8ToUTF16(title_))
                          .SetDesign(design_)
                          .SetProperty(
                              views::kFlexBehaviorKey,
                              views::FlexSpecification(
                                  views::MinimumFlexSizeRule::kPreferred,
                                  views::MaximumFlexSizeRule::kPreferred)))
                  .AddChild(
                      views::Builder<RetryView>()
                          .CopyAddressTo(&retry_view_)
                          .SetVisible(false)
                          .SetFirstLineText(base::UTF8ToUTF16(title_))
                          .SetRetryButtonCallback(base::BindRepeating(
                              &QuickAnswersUiController::OnRetryLabelPressed,
                              controller_))
                          .SetDesign(design_)
                          .SetProperty(
                              views::kFlexBehaviorKey,
                              views::FlexSpecification(
                                  views::MinimumFlexSizeRule::kPreferred,
                                  views::MaximumFlexSizeRule::kPreferred)))
                  .AddChild(views::Builder<ResultView>()
                                .CopyAddressTo(&result_view_)
                                .SetVisible(false)
                                .SetProperty(
                                    views::kFlexBehaviorKey,
                                    views::FlexSpecification(
                                        views::MinimumFlexSizeRule::kPreferred,
                                        views::MaximumFlexSizeRule::kPreferred,
                                        /*adjust_height_for_width=*/true))
                                .SetDesign(design_)
                                .SetGenerateTtsCallback(base::BindRepeating(
                                    &QuickAnswersView::GenerateTts,
                                    base::Unretained(this)))))
          .Build());

  // TODO(b/340629098): For `kRefresh` and `kMagicBoost`, buttons are in the
  // same row of a header. Move buttons to the row.
  AddChildView(
      views::Builder<views::BoxLayoutView>()
          .SetOrientation(views::LayoutOrientation::kHorizontal)
          .SetMainAxisAlignment(views::LayoutAlignment::kEnd)
          .SetCrossAxisAlignment(views::LayoutAlignment::kStart)
          .SetInsideBorderInsets(GetButtonsViewInsets(design_))
          .SetBetweenChildSpacing(kButtonsSpacingDip)
          .AddChild(
              views::Builder<views::ImageButton>()
                  .CopyAddressTo(&dogfood_button_)
                  .SetVisible(is_internal_)
                  .SetCallback(base::BindRepeating(
                      &QuickAnswersUiController::OnReportQueryButtonPressed,
                      controller_))
                  .SetTooltipText(l10n_util::GetStringUTF16(
                      IDS_QUICK_ANSWERS_DOGFOOD_FEEDBACK_BUTTON_TOOLTIP_TEXT))
                  .SetImageModel(
                      views::Button::STATE_NORMAL,
                      ui::ImageModel::FromVectorIcon(vector_icons::kDogfoodIcon,
                                                     ui::kColorIconSecondary,
                                                     kDogfoodButtonSizeDip)))
          .AddChild(
              views::Builder<views::ImageButton>()
                  .CopyAddressTo(&settings_button_)
                  .SetCallback(base::BindRepeating(
                      &QuickAnswersUiController::OnSettingsButtonPressed,
                      controller_))
                  .SetTooltipText(l10n_util::GetStringUTF16(
                      IDS_RICH_ANSWERS_VIEW_SETTINGS_BUTTON_A11Y_NAME_TEXT))
                  .SetImageModel(
                      views::Button::ButtonState::STATE_NORMAL,
                      ui::ImageModel::FromVectorIcon(
                          vector_icons::kSettingsOutlineIcon,
                          ui::kColorIconSecondary, kSettingsButtonSizeDip))
                  .SetProperty(views::kMarginsKey,
                               gfx::Insets(kSettingsButtonBorderDip)))
          .Build());

  SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
  set_suppress_default_focus_handling();

  GetViewAccessibility().SetRole(ax::mojom::Role::kDialog);
  UpdateAccessibleName();
  UpdateIcon();
  UpdateUiText();
}

QuickAnswersView::~QuickAnswersView() = default;

void QuickAnswersView::RequestFocus() {
  // When the Quick Answers view is focused, we actually want `main_view_`
  // to have the focus for highlight and selection purposes.
  quick_answers_stage_button_->RequestFocus();
}

bool QuickAnswersView::HasFocus() const {
  // When the Quick Answers view is focused, `main_view_` should have
  // the focus.
  return quick_answers_stage_button_->HasFocus();
}

void QuickAnswersView::OnFocus() {
  View* wants_focus = focus_search_->FindNextFocusableView(
      nullptr, views::FocusSearch::SearchDirection::kForwards,
      views::FocusSearch::TraversalDirection::kDown,
      views::FocusSearch::StartingViewPolicy::kCheckStartingView,
      views::FocusSearch::AnchoredDialogPolicy::kSkipAnchoredDialog, nullptr,
      nullptr);
  if (wants_focus != this) {
    wants_focus->RequestFocus();
  } else {
    NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true);
  }
}

views::FocusTraversable* QuickAnswersView::GetPaneFocusTraversable() {
  return focus_search_.get();
}

void QuickAnswersView::UpdateUiText() {
  if (design_ != Design::kRefresh) {
    return;
  }

  refreshed_ui_header_->SetText(GetIntentName(intent_));
}

void QuickAnswersView::UpdateAccessibleName() {
  // The view itself is not focused for retry-mode, so should not be announced
  // by the screen reader.
  if (retry_view_->GetVisible()) {
    GetViewAccessibility().SetName(
        std::u16string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
    return;
  }

  GetViewAccessibility().SetName(
      l10n_util::GetStringUTF8(IDS_QUICK_ANSWERS_VIEW_A11Y_NAME_TEXT));
}

void QuickAnswersView::UpdateIcon() {
  icon_->SetProperty(views::kMarginsKey, GetIconInsets(design_));
  icon_->SetImage(GetIcon(design_, intent_));
}

gfx::Size QuickAnswersView::GetMaximumSize() const {
  // TODO(b/340629098): update this. Different line heights are used for
  // `kRefresh` and `kMagicBoost`.
  int maximum_height =
      GetMainViewInsets(design_).height() + kContentViewInsets.height() +
      kDefaultLineHeightDip + kLineSpacingDip +
      quick_answers::ResultView::kMaxLines * kDefaultLineHeightDip;

  // The maximum height will be used in calculating the position of the widget
  // in `ReadWriteCardsUiController`. We need to reserve space at
  // the top since the view might expand for two-line answers.
  // Note that the width will not be used in the calculation.
  return gfx::Size(0, maximum_height);
}

void QuickAnswersView::UpdateBoundsForQuickAnswers() {
  // TODO(b/331271987): Remove this and the interface.
}

void QuickAnswersView::SendQuickAnswersQuery() {
  if (controller_) {
    controller_->OnQuickAnswersViewPressed();
  }
}

void QuickAnswersView::SwitchTo(views::View* view) {
  CHECK(view == loading_view_ || view == retry_view_ || view == result_view_);

  loading_view_->SetVisible(view == loading_view_);
  retry_view_->SetVisible(view == retry_view_);
  UpdateAccessibleName();
  result_view_->SetVisible(view == result_view_);
}

void QuickAnswersView::ShowRetryView() {
  SwitchTo(retry_view_);
}

bool QuickAnswersView::ShouldAddPhoneticsAudioButton(ResultType result_type,
                                                     GURL phonetics_audio,
                                                     bool tts_audio_enabled) {
  if (result_type != ResultType::kDefinitionResult) {
    return false;
  }

  return !phonetics_audio.is_empty() || tts_audio_enabled;
}

void QuickAnswersView::SetMockGenerateTtsCallbackForTesting(
    QuickAnswersView::MockGenerateTtsCallback mock_generate_tts_callback) {
  CHECK_IS_TEST();
  CHECK(!mock_generate_tts_callback.is_null());
  CHECK(mock_generate_tts_callback_.is_null());
  mock_generate_tts_callback_ = mock_generate_tts_callback;
}

bool QuickAnswersView::HasFocusInside() {
  views::FocusManager* focus_manager = GetFocusManager();
  if (!focus_manager) {
    // `focus_manager` can be nullptr only in a pixel test.
    CHECK_IS_TEST();
    return false;
  }

  return Contains(focus_manager->GetFocusedView());
}

void QuickAnswersView::SetIntent(Intent intent) {
  intent_ = intent;

  UpdateUiText();
  UpdateIcon();
}

std::optional<Intent> QuickAnswersView::GetIntent() const {
  return intent_;
}

void QuickAnswersView::SetResult(const StructuredResult& structured_result) {
  // Check if the view (or any of its children) had focus before resetting the
  // view, so it can be restored for the updated view.
  bool pane_already_had_focus = HasFocusInside();

  SwitchTo(result_view_);

  switch (structured_result.GetResultType()) {
    case ResultType::kDefinitionResult:
      SetIntent(Intent::kDefinition);
      SetResultTo(result_view_, structured_result.definition_result.get());
      break;
    case ResultType::kTranslationResult:
      SetIntent(Intent::kTranslation);
      SetResultTo(result_view_, structured_result.translation_result.get(),
                  design_);
      break;
    case ResultType::kUnitConversionResult:
      SetIntent(Intent::kUnitConversion);
      SetResultTo(result_view_, structured_result.unit_conversion_result.get());
      break;
    case ResultType::kNoResult:
      // Do not set intent back to unknown (i.e., `kNoResult`). See the comment
      // of `SetIntent` in the header file.
      SetNoResult(result_view_, title_);
      break;
  }

  GetViewAccessibility().SetDescription(result_view_->GetA11yDescription());

  // Restore focus if the view had one prior to updating the answer.
  if (pane_already_had_focus) {
    RequestFocus();
  } else {
    // Announce that a Quick Answer is available.
    GetViewAccessibility().AnnounceText(
        l10n_util::GetStringUTF16(IDS_QUICK_ANSWERS_VIEW_A11Y_INFO_ALERT_TEXT));
  }
}

// TODO(b/335701090): Move this out from QuickAnswersView to the controller.
void QuickAnswersView::GenerateTts(const PhoneticsInfo& phonetics_info) {
  if (!mock_generate_tts_callback_.is_null()) {
    CHECK_IS_TEST();
    mock_generate_tts_callback_.Run(phonetics_info);
    return;
  }

  if (!phonetics_audio_web_view_) {
    // Setup an invisible WebView to play phonetics audio.
    std::unique_ptr<views::WebView> web_view = std::make_unique<views::WebView>(
        ProfileManager::GetActiveUserProfile());
    web_view->SetVisible(false);
    phonetics_audio_web_view_.SetView(AddChildView(std::move(web_view)));
  }

  views::WebView* phonetics_audio_web_view =
      views::AsViewClass<views::WebView>(phonetics_audio_web_view_.view());
  CHECK(phonetics_audio_web_view);

  // Use the phonetics audio URL if provided.
  if (!phonetics_info.phonetics_audio.is_empty()) {
    phonetics_audio_web_view->LoadInitialURL(phonetics_info.phonetics_audio);
    return;
  }

  GenerateTTSAudio(phonetics_audio_web_view->GetBrowserContext(),
                   phonetics_info.query_text, phonetics_info.locale);
}

std::vector<views::View*> QuickAnswersView::GetFocusableViews() {
  std::vector<views::View*> focusable_views;
  // The main view does not gain focus for retry-view and transfers it to the
  // retry-label, and so is not included when this is the case.
  if (!retry_view_->GetVisible()) {
    focusable_views.push_back(quick_answers_stage_button_);
  }

  if (dogfood_button_ && dogfood_button_->GetVisible()) {
    focusable_views.push_back(dogfood_button_);
  }

  if (settings_button_ && settings_button_->GetVisible()) {
    focusable_views.push_back(settings_button_);
  }

  if (retry_view_->GetVisible()) {
    focusable_views.push_back(retry_view_->retry_label_button());
  }

  if (result_view_->GetVisible() &&
      result_view_->phonetics_audio_button()->GetVisible()) {
    focusable_views.push_back(result_view_->phonetics_audio_button());
  }

  return focusable_views;
}

BEGIN_METADATA(QuickAnswersView)
ADD_READONLY_PROPERTY_METADATA(std::optional<Intent>, Intent)
END_METADATA

}  // namespace quick_answers