Skip to content

Commit

Permalink
QA Rich Card: add translation child view
Browse files Browse the repository at this point in the history
Differentiate between content shown on the rich card view using the
quick answer result type. Added the RichAnswersTranslationView class,
views for definition and unit conversion will be added next.

--gtest_filter=*RichAnswersBrowserTest* --enable-pixel-output-in-tests`

Tests: tested on DUT, `out/Default/browser_tests
Bug: b/265258270
Change-Id: I2623bd448ebaace97de7fa3a1f973f1456f063a3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4347826
Reviewed-by: Xiaohui Chen <xiaohuic@chromium.org>
Commit-Queue: Angela Xiao <angelaxiao@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1121416}
  • Loading branch information
Angela Xiao authored and Chromium LUCI CQ committed Mar 23, 2023
1 parent 1553cfa commit d4bb172
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 16 deletions.
2 changes: 2 additions & 0 deletions chrome/browser/ui/quick_answers/BUILD.gn
Expand Up @@ -20,6 +20,8 @@ source_set("quick_answers") {
"ui/quick_answers_view.h",
"ui/rich_answers_pre_target_handler.cc",
"ui/rich_answers_pre_target_handler.h",
"ui/rich_answers_translation_view.cc",
"ui/rich_answers_translation_view.h",
"ui/rich_answers_view.cc",
"ui/rich_answers_view.h",
"ui/user_consent_view.cc",
Expand Down
56 changes: 55 additions & 1 deletion chrome/browser/ui/quick_answers/quick_answers_browsertest.cc
Expand Up @@ -16,16 +16,20 @@
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/quick_answers/quick_answers_browsertest_base.h"
#include "chrome/browser/ui/quick_answers/quick_answers_controller_impl.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_view.h"
#include "chrome/browser/ui/quick_answers/ui/rich_answers_view.h"
#include "chrome/browser/ui/quick_answers/ui/user_consent_view.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/components/quick_answers/public/cpp/quick_answers_prefs.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/message_center/public/cpp/notification.h"
Expand Down Expand Up @@ -225,18 +229,35 @@ IN_PROC_BROWSER_TEST_F(RichAnswersBrowserTest,
quick_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(quick_answers_view_widget != nullptr);

// Simulate having received a valid QuickAnswer response, which is necessary
// for triggering the rich answers view.
std::unique_ptr<quick_answers::QuickAnswer> quick_answer =
std::make_unique<quick_answers::QuickAnswer>();
quick_answer->result_type = ResultType::kTranslationResult;
quick_answer->title.push_back(
std::make_unique<quick_answers::QuickAnswerText>(
l10n_util::GetStringFUTF8(IDS_QUICK_ANSWERS_TRANSLATION_TITLE_TEXT,
u"prodotto", u"Italian")));
quick_answer->first_answer_row.push_back(
std::make_unique<quick_answers::QuickAnswerResultText>(
l10n_util::GetStringUTF8(IDS_QUICK_ANSWERS_TRANSLATION_INTENT)));
controller()->GetQuickAnswersDelegate()->OnQuickAnswerReceived(
std::move(quick_answer));

// Click on the quick answers view to trigger the rich answers view.
views::NamedWidgetShownWaiter rich_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), RichAnswersView::kWidgetName);
event_generator_->MoveMouseTo(
quick_answers_view_widget->GetWindowBoundsInScreen().CenterPoint());
event_generator_->ClickLeftButton();

// Check that the quick answers view closes and the rich answers view pops up.
// Check that the quick answers view closes when the rich answers view shows.
views::Widget* rich_answers_view_widget =
rich_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(quick_answers_view_widget->IsClosed());
ASSERT_TRUE(rich_answers_view_widget != nullptr);
ASSERT_TRUE(controller()->GetVisibilityForTesting() ==
QuickAnswersVisibility::kRichAnswersVisible);

// Click outside the rich answers view window bounds to dismiss it.
gfx::Rect rich_answers_bounds =
Expand All @@ -249,4 +270,37 @@ IN_PROC_BROWSER_TEST_F(RichAnswersBrowserTest,
ASSERT_TRUE(rich_answers_view_widget->IsClosed());
}

IN_PROC_BROWSER_TEST_F(RichAnswersBrowserTest,
RichAnswersNotTriggeredOnInvalidResult) {
std::unique_ptr<ui::test::EventGenerator> event_generator_ =
std::make_unique<ui::test::EventGenerator>(
ash::Shell::GetPrimaryRootWindow());

views::NamedWidgetShownWaiter quick_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), QuickAnswersView::kWidgetName);

ShowMenuParams params;
params.selected_text = kTestQuery;
params.x = kCursorXToOverlapWithANotification;
params.y = kCursorYToOverlapWithANotification;
ShowMenu(params);

views::Widget* quick_answers_view_widget =
quick_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(quick_answers_view_widget != nullptr);

// Click on the quick answers view. This should not trigger the
// rich answers view since no valid QuickAnswer result is provided.
views::NamedWidgetShownWaiter rich_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), RichAnswersView::kWidgetName);
event_generator_->MoveMouseTo(
quick_answers_view_widget->GetWindowBoundsInScreen().CenterPoint());
event_generator_->ClickLeftButton();

// Check that all quick answers views are closed.
ASSERT_TRUE(quick_answers_view_widget->IsClosed());
ASSERT_TRUE(controller()->GetVisibilityForTesting() ==
QuickAnswersVisibility::kClosed);
}

} // namespace quick_answers
Expand Up @@ -71,6 +71,8 @@ class QuickAnswersControllerImpl : public QuickAnswersController,
return quick_answers_ui_controller_.get();
}

quick_answers::QuickAnswer* quick_answer() { return quick_answer_.get(); }

private:
void HandleQuickAnswerRequest(
const quick_answers::QuickAnswersRequest& request);
Expand Down
11 changes: 6 additions & 5 deletions chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc
Expand Up @@ -102,15 +102,16 @@ void QuickAnswersUiController::OnQuickAnswersViewPressed() {
// Route dismissal through |controller_| for logging impressions.
controller_->DismissQuickAnswers(QuickAnswersExitPoint::kQuickAnswersClick);

if (chromeos::features::IsQuickAnswersRichCardEnabled()) {
if (chromeos::features::IsQuickAnswersRichCardEnabled() &&
controller_->quick_answer() != nullptr &&
controller_->quick_answer()->result_type !=
quick_answers::ResultType::kNoResult) {
auto* const rich_answers_view = new quick_answers::RichAnswersView(
quick_answers_view_tracker_.view()->bounds(),
weak_factory_.GetWeakPtr());
rich_answers_view_tracker_.SetView(rich_answers_view);
weak_factory_.GetWeakPtr(), *controller_->quick_answer());
rich_answers_view->GetWidget()->ShowInactive();

// Temporarily set rich-answers visibility here. Will move after
// setting up rich-answers request handling.
rich_answers_view_tracker_.SetView(rich_answers_view);
controller_->SetVisibility(QuickAnswersVisibility::kRichAnswersVisible);
return;
}
Expand Down
@@ -0,0 +1,83 @@
// 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/rich_answers_translation_view.h"

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/quick_answers/quick_answers_ui_controller.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/display/screen.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.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/image_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"

// RichAnswersTranslationView
// -----------------------------------------------------------

RichAnswersTranslationView::RichAnswersTranslationView(
const quick_answers::QuickAnswer& result) {
InitLayout();

// Focus.
// We use custom focus behavior for the quick answers views.
// TODO (b/274665781): Add unit tests for focus behavior.
SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
set_suppress_default_focus_handling();
focus_search_ = std::make_unique<QuickAnswersFocusSearch>(
this, base::BindRepeating(&RichAnswersTranslationView::GetFocusableViews,
base::Unretained(this)));
}

RichAnswersTranslationView::~RichAnswersTranslationView() = default;

const char* RichAnswersTranslationView::GetClassName() const {
return "RichAnswersTranslationView";
}

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

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

void RichAnswersTranslationView::InitLayout() {
SetLayoutManager(std::make_unique<views::FillLayout>());
}

std::vector<views::View*> RichAnswersTranslationView::GetFocusableViews() {
std::vector<views::View*> focusable_views;
focusable_views.push_back(this);

return focusable_views;
}
52 changes: 52 additions & 0 deletions chrome/browser/ui/quick_answers/ui/rich_answers_translation_view.h
@@ -0,0 +1,52 @@
// 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.

#ifndef CHROME_BROWSER_UI_QUICK_ANSWERS_UI_RICH_ANSWERS_TRANSLATION_VIEW_H_
#define CHROME_BROWSER_UI_QUICK_ANSWERS_UI_RICH_ANSWERS_TRANSLATION_VIEW_H_

#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_focus_search.h"
#include "chrome/browser/ui/quick_answers/ui/rich_answers_view.h"
#include "ui/events/event_handler.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view.h"

namespace views {
class ImageButton;
} // namespace views

// A bubble style view to show QuickAnswer.
class RichAnswersTranslationView : public views::View {
public:
explicit RichAnswersTranslationView(const quick_answers::QuickAnswer& result);

RichAnswersTranslationView(const RichAnswersTranslationView&) = delete;
RichAnswersTranslationView& operator=(const RichAnswersTranslationView&) =
delete;

~RichAnswersTranslationView() override;

// views::View:
const char* GetClassName() const override;
void OnFocus() override;
views::FocusTraversable* GetPaneFocusTraversable() override;

private:
void InitLayout();

// QuickAnswersFocusSearch::GetFocusableViewsCallback to poll currently
// focusable views.
std::vector<views::View*> GetFocusableViews();

raw_ptr<views::View> base_view_ = nullptr;
raw_ptr<views::ImageButton> settings_button_ = nullptr;

std::unique_ptr<QuickAnswersFocusSearch> focus_search_;
base::WeakPtrFactory<RichAnswersTranslationView> weak_factory_{this};
};

#endif // CHROME_BROWSER_UI_QUICK_ANSWERS_UI_QUICK_ANSWERS_VIEW_H_
49 changes: 43 additions & 6 deletions chrome/browser/ui/quick_answers/ui/rich_answers_view.cc
Expand Up @@ -8,7 +8,10 @@
#include "base/memory/raw_ptr.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/quick_answers_view.h"
#include "chrome/browser/ui/quick_answers/ui/rich_answers_pre_target_handler.h"
#include "chrome/browser/ui/quick_answers/ui/rich_answers_translation_view.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/aura/window.h"
Expand All @@ -24,13 +27,18 @@
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace {

using quick_answers::QuickAnswer;
using quick_answers::QuickAnswerResultText;
using quick_answers::ResultType;

// Buttons view.
constexpr int kButtonsViewMarginDip = 4;
constexpr int kButtonsSpacingDip = 4;
Expand All @@ -44,11 +52,28 @@ constexpr int kBorderCornerRadius = 12;

namespace quick_answers {

class RichAnswersTextLabel : public views::Label {
public:
explicit RichAnswersTextLabel(
const std::u16string& text,
ui::ColorId color_id = ui::kColorLabelForeground)
: Label(text) {
SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
SetEnabledColorId(color_id);
}

RichAnswersTextLabel(const RichAnswersTextLabel&) = delete;
RichAnswersTextLabel& operator=(const RichAnswersTextLabel&) = delete;

~RichAnswersTextLabel() override = default;
};

// RichAnswersView -----------------------------------------------------------

RichAnswersView::RichAnswersView(
const gfx::Rect& anchor_view_bounds,
base::WeakPtr<QuickAnswersUiController> controller)
base::WeakPtr<QuickAnswersUiController> controller,
const quick_answers::QuickAnswer& result)
: anchor_view_bounds_(anchor_view_bounds),
controller_(std::move(controller)),
rich_answers_view_handler_(
Expand All @@ -57,7 +82,7 @@ RichAnswersView::RichAnswersView(
this,
base::BindRepeating(&RichAnswersView::GetFocusableViews,
base::Unretained(this)))) {
InitLayout();
InitLayout(result);
InitWidget();

// Focus.
Expand Down Expand Up @@ -110,10 +135,10 @@ void RichAnswersView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ax::mojom::Role::kDialog;

node_data->SetName(
l10n_util::GetStringUTF8(IDS_QUICK_ANSWERS_VIEW_A11Y_NAME_TEXT));
l10n_util::GetStringUTF8(IDS_RICH_ANSWERS_VIEW_A11Y_NAME_TEXT));
}

void RichAnswersView::InitLayout() {
void RichAnswersView::InitLayout(const quick_answers::QuickAnswer& result) {
SetLayoutManager(std::make_unique<views::FillLayout>());

// Base view Layout.
Expand All @@ -123,10 +148,22 @@ void RichAnswersView::InitLayout() {
base_layout->SetOrientation(views::LayoutOrientation::kVertical)
.SetCrossAxisAlignment(views::LayoutAlignment::kStretch);

// TODO(b/259440976): Add child views for each result type.

// Add util buttons in the top-right corner.
AddFrameButtons();

switch (result.result_type) {
case quick_answers::ResultType::kTranslationResult: {
content_view_ = base_view_->AddChildView(
std::make_unique<RichAnswersTranslationView>(result));
return;
}
case quick_answers::ResultType::kDefinitionResult:
case quick_answers::ResultType::kUnitConversionResult:
default: {
// TODO(b/259440976): Add child views for each result type.
return;
}
}
}

void RichAnswersView::InitWidget() {
Expand Down

0 comments on commit d4bb172

Please sign in to comment.