From 2f90be4eb9d25ec8e2e32de9a24fc840af0bf180 Mon Sep 17 00:00:00 2001 From: Yue Li Date: Sat, 28 Jan 2023 00:01:05 +0000 Subject: [PATCH] QA Rich card: Create Rich answers card UI This change create an empty view for Rich answers card. Contents of each answers type will be added in follow-up changes. Bug: b/259449788 Test: None Change-Id: I3fd95a93cadd761e02b6f27e0710603ae6846bfe Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4159811 Reviewed-by: Tao Wu Commit-Queue: Angela Xiao Reviewed-by: Angela Xiao Cr-Commit-Position: refs/heads/main@{#1098192} --- chrome/browser/ui/quick_answers/BUILD.gn | 3 + .../quick_answers_ui_controller.cc | 12 ++ .../quick_answers_ui_controller.h | 3 +- .../ui/quick_answers/ui/rich_answers_view.cc | 182 ++++++++++++++++++ .../ui/quick_answers/ui/rich_answers_view.h | 62 ++++++ 5 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 chrome/browser/ui/quick_answers/ui/rich_answers_view.cc create mode 100644 chrome/browser/ui/quick_answers/ui/rich_answers_view.h diff --git a/chrome/browser/ui/quick_answers/BUILD.gn b/chrome/browser/ui/quick_answers/BUILD.gn index 5dfb9a5ad8fa2..6f2e6111f1303 100644 --- a/chrome/browser/ui/quick_answers/BUILD.gn +++ b/chrome/browser/ui/quick_answers/BUILD.gn @@ -18,6 +18,8 @@ source_set("quick_answers") { "ui/quick_answers_pre_target_handler.h", "ui/quick_answers_view.cc", "ui/quick_answers_view.h", + "ui/rich_answers_view.cc", + "ui/rich_answers_view.h", "ui/user_consent_view.cc", "ui/user_consent_view.h", ] @@ -27,6 +29,7 @@ source_set("quick_answers") { "//chromeos/components/quick_answers", "//chromeos/components/quick_answers/public/cpp:cpp", "//chromeos/components/quick_answers/public/cpp:prefs", + "//chromeos/constants:constants", "//chromeos/strings:strings_grit", "//components/account_id", "//components/prefs", diff --git a/chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc b/chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc index bb327bc925ab0..f4cb8b98b2e66 100644 --- a/chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc +++ b/chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc @@ -9,8 +9,10 @@ #include "base/strings/stringprintf.h" #include "build/chromeos_buildflags.h" #include "chrome/browser/ui/quick_answers/quick_answers_controller_impl.h" +#include "chrome/browser/ui/quick_answers/ui/rich_answers_view.h" #include "chromeos/components/quick_answers/public/cpp/quick_answers_state.h" #include "chromeos/components/quick_answers/quick_answers_model.h" +#include "chromeos/constants/chromeos_features.h" #include "chromeos/strings/grit/chromeos_strings.h" #include "mojo/public/cpp/bindings/remote.h" #include "third_party/abseil-cpp/absl/types/optional.h" @@ -97,6 +99,16 @@ void QuickAnswersUiController::CreateQuickAnswersView(const gfx::Rect& bounds, } void QuickAnswersUiController::OnQuickAnswersViewPressed() { + if (chromeos::features::IsQuickAnswersRichCardEnabled()) { + auto* const rich_answers_view = + new RichAnswersView(quick_answers_view_tracker_.view()->bounds(), + weak_factory_.GetWeakPtr()); + rich_answers_view_tracker_.SetView(rich_answers_view); + rich_answers_view->GetWidget()->ShowInactive(); + controller_->DismissQuickAnswers(QuickAnswersExitPoint::kQuickAnswersClick); + return; + } + // Route dismissal through |controller_| for logging impressions. controller_->DismissQuickAnswers(QuickAnswersExitPoint::kQuickAnswersClick); diff --git a/chrome/browser/ui/quick_answers/quick_answers_ui_controller.h b/chrome/browser/ui/quick_answers/quick_answers_ui_controller.h index 9265fdcc1e461..6ba3c1dc9e7e7 100644 --- a/chrome/browser/ui/quick_answers/quick_answers_ui_controller.h +++ b/chrome/browser/ui/quick_answers/quick_answers_ui_controller.h @@ -99,9 +99,10 @@ class QuickAnswersUiController { private: raw_ptr controller_ = nullptr; - // Trackers for quick answers and user consent view. + // Trackers for quick answers related views. views::ViewTracker quick_answers_view_tracker_; views::ViewTracker user_consent_view_tracker_; + views::ViewTracker rich_answers_view_tracker_; std::string query_; diff --git a/chrome/browser/ui/quick_answers/ui/rich_answers_view.cc b/chrome/browser/ui/quick_answers/ui/rich_answers_view.cc new file mode 100644 index 0000000000000..de2c39537a8fe --- /dev/null +++ b/chrome/browser/ui/quick_answers/ui/rich_answers_view.cc @@ -0,0 +1,182 @@ +// 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_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 "chrome/browser/ui/quick_answers/ui/quick_answers_pre_target_handler.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/layout/flex_layout.h" +#include "ui/views/widget/widget.h" +#include "ui/wm/core/coordinate_conversion.h" + +namespace { + +// Buttons view. +constexpr int kButtonsViewMarginDip = 4; +constexpr int kButtonsSpacingDip = 4; +constexpr int kSettingsButtonSizeDip = 14; +constexpr int kSettingsButtonBorderDip = 3; + +} // namespace + +// RichAnswersView ----------------------------------------------------------- + +RichAnswersView::RichAnswersView( + const gfx::Rect& anchor_view_bounds, + base::WeakPtr controller) + : anchor_view_bounds_(anchor_view_bounds), + controller_(std::move(controller)), + focus_search_(std::make_unique( + this, + base::BindRepeating(&RichAnswersView::GetFocusableViews, + base::Unretained(this)))) { + InitLayout(); + InitWidget(); + + // Focus. + SetFocusBehavior(views::View::FocusBehavior::ALWAYS); + set_suppress_default_focus_handling(); +} + +RichAnswersView::~RichAnswersView() = default; + +const char* RichAnswersView::GetClassName() const { + return "RichAnswersView"; +} + +void RichAnswersView::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); + } +} + +void RichAnswersView::OnThemeChanged() { + views::View::OnThemeChanged(); + SetBackground(views::CreateSolidBackground( + GetColorProvider()->GetColor(ui::kColorPrimaryBackground))); + if (settings_button_) { + settings_button_->SetImage( + views::Button::ButtonState::STATE_NORMAL, + gfx::CreateVectorIcon( + vector_icons::kSettingsOutlineIcon, kSettingsButtonSizeDip, + GetColorProvider()->GetColor(ui::kColorIconSecondary))); + } +} + +views::FocusTraversable* RichAnswersView::GetPaneFocusTraversable() { + return focus_search_.get(); +} + +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)); +} + +void RichAnswersView::InitLayout() { + SetLayoutManager(std::make_unique()); + + // Base view Layout. + base_view_ = AddChildView(std::make_unique()); + auto* base_layout = + base_view_->SetLayoutManager(std::make_unique()); + 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(); +} + +void RichAnswersView::InitWidget() { + views::Widget::InitParams params; + params.activatable = views::Widget::InitParams::Activatable::kNo; + params.shadow_elevation = 2; + params.shadow_type = views::Widget::InitParams::ShadowType::kDrop; + params.type = views::Widget::InitParams::TYPE_POPUP; + params.z_order = ui::ZOrderLevel::kFloatingUIElement; + + views::Widget* widget = new views::Widget(); + widget->Init(std::move(params)); + widget->SetContentsView(this); + UpdateBounds(); +} + +void RichAnswersView::AddFrameButtons() { + auto* buttons_view = AddChildView(std::make_unique()); + auto* layout = + buttons_view->SetLayoutManager(std::make_unique()); + layout->SetOrientation(views::LayoutOrientation::kHorizontal) + .SetMainAxisAlignment(views::LayoutAlignment::kEnd) + .SetCrossAxisAlignment(views::LayoutAlignment::kStart) + .SetInteriorMargin(gfx::Insets(kButtonsViewMarginDip)) + .SetDefault(views::kMarginsKey, + gfx::Insets::TLBR(0, kButtonsSpacingDip, 0, 0)); + + settings_button_ = buttons_view->AddChildView( + std::make_unique(base::BindRepeating( + &QuickAnswersUiController::OnSettingsButtonPressed, controller_))); + settings_button_->SetTooltipText(l10n_util::GetStringUTF16( + IDS_QUICK_ANSWERS_SETTINGS_BUTTON_TOOLTIP_TEXT)); + settings_button_->SetBorder( + views::CreateEmptyBorder(kSettingsButtonBorderDip)); +} + +void RichAnswersView::UpdateBounds() { + auto display_bounds = display::Screen::GetScreen() + ->GetDisplayMatching(anchor_view_bounds_) + .bounds(); + + // TODO(b/259440976): Calculate desired bounds based on anchor view bounds. + gfx::Rect bounds = { + {display_bounds.width() / 2 - 200, display_bounds.height() / 2 - 300}, + {400, 600}}; +#if BUILDFLAG(IS_CHROMEOS_ASH) + // For Ash, convert the position relative to the screen. + // For Lacros, `bounds` is already relative to the toplevel window and the + // position will be calculated on server side. + wm::ConvertRectFromScreen(GetWidget()->GetNativeWindow()->parent(), &bounds); +#endif + GetWidget()->SetBounds(bounds); +} + +std::vector RichAnswersView::GetFocusableViews() { + std::vector focusable_views; + focusable_views.push_back(this); + + if (settings_button_ && settings_button_->GetVisible()) { + focusable_views.push_back(settings_button_); + } + + return focusable_views; +} diff --git a/chrome/browser/ui/quick_answers/ui/rich_answers_view.h b/chrome/browser/ui/quick_answers/ui/rich_answers_view.h new file mode 100644 index 0000000000000..d1789adf4e6c1 --- /dev/null +++ b/chrome/browser/ui/quick_answers/ui/rich_answers_view.h @@ -0,0 +1,62 @@ +// 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_VIEW_H_ +#define CHROME_BROWSER_UI_QUICK_ANSWERS_UI_RICH_ANSWERS_VIEW_H_ + +#include + +#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 "ui/events/event_handler.h" +#include "ui/views/focus/focus_manager.h" +#include "ui/views/view.h" + +namespace views { +class ImageButton; +} // namespace views + +class QuickAnswersUiController; + +// A bubble style view to show QuickAnswer. +class RichAnswersView : public views::View { + public: + RichAnswersView(const gfx::Rect& anchor_view_bounds, + base::WeakPtr controller); + + RichAnswersView(const RichAnswersView&) = delete; + RichAnswersView& operator=(const RichAnswersView&) = delete; + + ~RichAnswersView() override; + + // views::View: + const char* GetClassName() const override; + void OnFocus() override; + void OnThemeChanged() override; + views::FocusTraversable* GetPaneFocusTraversable() override; + void GetAccessibleNodeData(ui::AXNodeData* node_data) override; + + private: + void InitLayout(); + void InitWidget(); + void AddFrameButtons(); + void UpdateBounds(); + + // QuickAnswersFocusSearch::GetFocusableViewsCallback to poll currently + // focusable views. + std::vector GetFocusableViews(); + + gfx::Rect anchor_view_bounds_; + + base::WeakPtr controller_; + + raw_ptr base_view_ = nullptr; + raw_ptr settings_button_ = nullptr; + + std::unique_ptr focus_search_; + base::WeakPtrFactory weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_UI_QUICK_ANSWERS_UI_QUICK_ANSWERS_VIEW_H_