// Copyright 2023 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_util.h"
#include "base/strings/escape.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_text_label.h"
#include "chromeos/components/quick_answers/public/cpp/quick_answers_state.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/vector_icons/vector_icons.h"
#include "content/browser/speech/tts_controller_impl.h"
#include "content/public/browser/tts_utterance.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_id.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/separator.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_view.h"
namespace {
// Spacing between labels in the horizontal elements view.
constexpr int kLabelSpacingDip = 2;
// Google search link.
constexpr char kGoogleSearchUrlPrefix[] = "https://www.google.com/search?q=";
constexpr char kGoogleTranslateUrlTemplate[] =
"https://translate.google.com/?sl=auto&tl=%s&text=%s&op=translate";
constexpr char kTranslationQueryPrefix[] = "Translate:";
} // namespace
namespace quick_answers {
using views::View;
void QuickAnswersUtteranceEventDelegate::OnTtsEvent(
content::TtsUtterance* utterance,
content::TtsEventType event_type,
int char_index,
int char_length,
const std::string& error_message) {
// For quick answers, the TTS events of interest are START, END, and ERROR.
switch (event_type) {
case content::TTS_EVENT_START:
quick_answers::RecordTtsEngineEvent(
quick_answers::TtsEngineEvent::TTS_EVENT_START);
break;
case content::TTS_EVENT_END:
quick_answers::RecordTtsEngineEvent(
quick_answers::TtsEngineEvent::TTS_EVENT_END);
break;
case content::TTS_EVENT_ERROR:
VLOG(1) << __func__ << ": " << error_message;
quick_answers::RecordTtsEngineEvent(
quick_answers::TtsEngineEvent::TTS_EVENT_ERROR);
break;
case content::TTS_EVENT_WORD:
case content::TTS_EVENT_SENTENCE:
case content::TTS_EVENT_MARKER:
case content::TTS_EVENT_INTERRUPTED:
case content::TTS_EVENT_CANCELLED:
case content::TTS_EVENT_PAUSE:
case content::TTS_EVENT_RESUME:
// Group the remaining TTS events that aren't of interest together
// into an unspecified "other" category.
quick_answers::RecordTtsEngineEvent(
quick_answers::TtsEngineEvent::TTS_EVENT_OTHER);
break;
}
if (utterance->IsFinished()) {
delete this;
}
}
gfx::FontList GetFontList(TypographyToken token) {
std::vector<std::string> kGoogleSansFontFamily = {kGoogleSansFont,
kRobotoFont};
switch (token) {
case TypographyToken::kCrosBody2:
return gfx::FontList(kGoogleSansFontFamily, gfx::Font::NORMAL,
/*font_size=*/13, gfx::Font::Weight::NORMAL);
case TypographyToken::kCrosBody2Italic:
return gfx::FontList(kGoogleSansFontFamily, gfx::Font::ITALIC,
/*font_size=*/13, gfx::Font::Weight::NORMAL);
case TypographyToken::kCrosButton1:
return gfx::FontList(kGoogleSansFontFamily, gfx::Font::NORMAL,
/*font_size=*/14, gfx::Font::Weight::MEDIUM);
case TypographyToken::kCrosButton2:
return gfx::FontList(kGoogleSansFontFamily, gfx::Font::NORMAL,
/*font_size=*/13, gfx::Font::Weight::MEDIUM);
case TypographyToken::kCrosDisplay5:
return gfx::FontList(kGoogleSansFontFamily, gfx::Font::NORMAL,
/*font_size=*/24, gfx::Font::Weight::MEDIUM);
case TypographyToken::kCrosTitle1:
return gfx::FontList(kGoogleSansFontFamily, gfx::Font::NORMAL,
/*font_size=*/16, gfx::Font::Weight::MEDIUM);
}
}
const gfx::VectorIcon& GetResultTypeIcon(ResultType result_type) {
switch (result_type) {
case ResultType::kDefinitionResult:
return omnibox::kAnswerDictionaryIcon;
case ResultType::kTranslationResult:
return omnibox::kAnswerTranslationIcon;
case ResultType::kUnitConversionResult:
return omnibox::kAnswerCalculatorIcon;
default:
return omnibox::kAnswerDefaultIcon;
}
}
View* AddHorizontalUiElements(
View* container,
const std::vector<std::unique_ptr<QuickAnswerUiElement>>& elements) {
auto* labels_container = container->AddChildView(std::make_unique<View>());
auto* layout =
labels_container->SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetOrientation(views::LayoutOrientation::kHorizontal)
.SetDefault(views::kMarginsKey,
gfx::Insets::TLBR(0, 0, 0, kLabelSpacingDip));
for (const auto& element : elements) {
switch (element->type) {
case QuickAnswerUiElementType::kText:
labels_container->AddChildView(std::make_unique<QuickAnswersTextLabel>(
*static_cast<QuickAnswerText*>(element.get())));
break;
case QuickAnswerUiElementType::kImage:
// TODO(yanxiao): Add image view
break;
case QuickAnswerUiElementType::kUnknown:
LOG(ERROR) << "Trying to add an unknown QuickAnswerUiElement.";
break;
}
}
return labels_container;
}
std::unique_ptr<views::BoxLayoutView> CreateVerticalBoxLayoutView() {
return views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kVertical)
.SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kStretch)
.SetBetweenChildSpacing(kContentSingleSpacing)
.Build();
}
std::unique_ptr<views::BoxLayoutView> CreateHorizontalBoxLayoutView() {
return views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kHorizontal)
.SetBetweenChildSpacing(kContentSingleSpacing)
.Build();
}
std::unique_ptr<views::FlexLayoutView> CreateHorizontalFlexLayoutView() {
std::unique_ptr<views::FlexLayoutView> horizontal_view =
views::Builder<views::FlexLayoutView>()
.SetOrientation(views::LayoutOrientation::kHorizontal)
.Build();
horizontal_view->SetDefault(views::kMarginsKey,
kViewHorizontalSpacingMargins);
return horizontal_view;
}
std::unique_ptr<views::Separator> CreateSeparatorView() {
return views::Builder<views::Separator>()
.SetOrientation(views::Separator::Orientation::kHorizontal)
.SetColorId(ui::kColorSeparator)
.SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(kContentSingleSpacing, 0, kContentSingleSpacing, 0))
.Build();
}
std::unique_ptr<views::ImageButton> CreateImageButtonView(
base::RepeatingClosure closure,
ui::ImageModel image_model,
ui::ColorId background_color,
std::u16string tooltip_text) {
std::unique_ptr<views::ImageButton> image_button =
std::make_unique<views::ImageButton>(closure);
image_button->SetBackground(views::CreateThemedRoundedRectBackground(
background_color, kRichAnswersIconContainerRadius));
image_button->SetBorder(views::CreateEmptyBorder(kRichAnswersIconBorderDip));
image_button->SetImageModel(views::Button::ButtonState::STATE_NORMAL,
image_model);
image_button->SetTooltipText(tooltip_text);
return image_button;
}
GURL GetDetailsUrlForQuery(const std::string& query) {
// TODO(b/240619915): Refactor so that we can access the request metadata
// instead of just the query itself.
if (base::StartsWith(query, kTranslationQueryPrefix)) {
auto query_text = base::EscapeUrlEncodedData(
query.substr(strlen(kTranslationQueryPrefix)), /*use_plus=*/true);
auto device_language =
l10n_util::GetLanguage(QuickAnswersState::Get()->application_locale());
auto translate_url =
base::StringPrintf(kGoogleTranslateUrlTemplate, device_language.c_str(),
query_text.c_str());
return GURL(translate_url);
} else {
return GURL(kGoogleSearchUrlPrefix +
base::EscapeUrlEncodedData(query, /*use_plus=*/true));
}
}
void GenerateTTSAudio(content::BrowserContext* browser_context,
const std::string& text,
const std::string& locale) {
auto* tts_controller = content::TtsControllerImpl::GetInstance();
std::unique_ptr<content::TtsUtterance> tts_utterance =
content::TtsUtterance::Create(browser_context);
tts_controller->SetStopSpeakingWhenHidden(false);
tts_utterance->SetShouldClearQueue(false);
tts_utterance->SetText(text);
tts_utterance->SetLang(locale);
// TtsController will use the default TTS engine if the Google TTS engine
// is not available.
tts_utterance->SetEngineId(kGoogleTtsEngineId);
tts_utterance->SetEventDelegate(new QuickAnswersUtteranceEventDelegate());
tts_controller->SpeakOrEnqueue(std::move(tts_utterance));
}
} // namespace quick_answers