blob: 3a44e869b2a59b257658ba93d2916e3b2d522d70 [file] [log] [blame]
// 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 "ash/user_education/user_education_help_bubble_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/user_education/user_education_delegate.h"
#include "ash/user_education/user_education_types.h"
#include "ash/user_education/user_education_util.h"
#include "ash/user_education/views/help_bubble_factory_views_ash.h"
#include "ash/user_education/views/help_bubble_view_ash.h"
#include "base/cancelable_callback.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "components/account_id/account_id.h"
#include "components/user_education/common/help_bubble.h"
#include "components/user_education/common/help_bubble_params.h"
#include "ui/aura/window.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// The singleton instance owned by the `UserEducationController`.
UserEducationHelpBubbleController* g_instance = nullptr;
// Helpers ---------------------------------------------------------------------
gfx::Rect GetAnchorBoundsInScreen(const HelpBubbleViewAsh* help_bubble_view) {
return help_bubble_view->GetAnchorView()->GetAnchorBoundsInScreen();
}
aura::Window* GetAnchorRootWindow(const HelpBubbleViewAsh* help_bubble_view) {
return help_bubble_view->GetAnchorView()
->GetWidget()
->GetNativeWindow()
->GetRootWindow();
}
} // namespace
// UserEducationHelpBubbleController -------------------------------------------
UserEducationHelpBubbleController::UserEducationHelpBubbleController(
UserEducationDelegate* delegate)
: delegate_(delegate) {
CHECK_EQ(g_instance, nullptr);
g_instance = this;
}
UserEducationHelpBubbleController::~UserEducationHelpBubbleController() {
CHECK_EQ(g_instance, this);
g_instance = nullptr;
}
// static
UserEducationHelpBubbleController* UserEducationHelpBubbleController::Get() {
// Should only be `nullptr` in testing.
if (!g_instance) {
CHECK_IS_TEST();
}
return g_instance;
}
bool UserEducationHelpBubbleController::CreateHelpBubble(
HelpBubbleId help_bubble_id,
user_education::HelpBubbleParams help_bubble_params,
ui::ElementIdentifier element_id,
ui::ElementContext element_context,
base::OnceClosure close_callback) {
auto scoped_help_bubble_closer = CreateScopedHelpBubble(
help_bubble_id, std::move(help_bubble_params), element_id,
element_context, std::move(close_callback));
if (scoped_help_bubble_closer) {
std::ignore = scoped_help_bubble_closer.Release();
return true;
}
return false;
}
base::ScopedClosureRunner
UserEducationHelpBubbleController::CreateScopedHelpBubble(
HelpBubbleId help_bubble_id,
user_education::HelpBubbleParams help_bubble_params,
ui::ElementIdentifier element_id,
ui::ElementContext element_context,
base::OnceClosure close_callback) {
// Prohibit showing multiple help bubbles concurrently.
if (help_bubble_) {
return base::ScopedClosureRunner();
}
// NOTE: User education in Ash is currently only supported for the primary
// user profile. This is a self-imposed restriction.
auto account_id = Shell::Get()->session_controller()->GetActiveAccountId();
CHECK(user_education_util::IsPrimaryAccountId(account_id));
// Attempt to create a `help_bubble_`.
help_bubble_ = delegate_->CreateHelpBubble(account_id, help_bubble_id,
std::move(help_bubble_params),
element_id, element_context);
// The `delegate_` may opt *not* to create a `help_bubble_` in certain
// circumstances, e.g. when there is an ongoing tutorial.
if (!help_bubble_) {
return base::ScopedClosureRunner();
}
auto close_help_bubble_closure =
std::make_unique<base::CancelableOnceClosure>(base::BindOnce(
[](user_education::HelpBubble* help_bubble) { help_bubble->Close(); },
base::Unretained(help_bubble_.get())));
base::ScopedClosureRunner scoped_help_bubble_closer(
close_help_bubble_closure->callback());
// Subscribe to be notified when the `help_bubble_` closes. Once closed, free
// `help_bubble_` related memory, cancel the callback to close the bubble, and
// run the provided `close_callback`.
help_bubble_close_subscription_ =
help_bubble_->AddOnCloseCallback(base::BindOnce(
[](UserEducationHelpBubbleController* self,
base::OnceClosure close_callback, base::CancelableOnceClosure*,
user_education::HelpBubble* help_bubble) {
CHECK_EQ(self->help_bubble_.get(), help_bubble);
self->help_bubble_.reset();
self->help_bubble_close_subscription_ =
base::CallbackListSubscription();
std::move(close_callback).Run();
},
base::Unretained(this), std::move(close_callback),
base::Owned(std::move(close_help_bubble_closure))));
return scoped_help_bubble_closer;
}
std::optional<HelpBubbleId> UserEducationHelpBubbleController::GetHelpBubbleId(
ui::ElementIdentifier element_id,
ui::ElementContext element_context) const {
if (help_bubble_ && help_bubble_->IsA<HelpBubbleViewsAsh>()) {
// Cache the `bubble_view` with its associated anchor.
auto* bubble_view = help_bubble_->AsA<HelpBubbleViewsAsh>()->bubble_view();
auto* anchor_view = bubble_view->GetAnchorView();
// Find all `tracked_views` matching `element_id` and `element_context`.
const views::ElementTrackerViews::ViewList tracked_views =
views::ElementTrackerViews::GetInstance()->GetAllMatchingViews(
element_id, element_context);
// A help bubble exists for a `tracked_view` if the `tracked_view` is the
// `anchor_view` for the help bubble.
for (const auto* tracked_view : tracked_views) {
if (tracked_view == anchor_view) {
return bubble_view->id();
}
}
}
return std::nullopt;
}
base::CallbackListSubscription
UserEducationHelpBubbleController::AddHelpBubbleAnchorBoundsChangedCallback(
base::RepeatingClosure callback) {
return help_bubble_anchor_bounds_changed_subscribers_.Add(
std::move(callback));
}
base::CallbackListSubscription
UserEducationHelpBubbleController::AddHelpBubbleClosedCallback(
base::RepeatingClosure callback) {
return help_bubble_closed_subscribers_.Add(std::move(callback));
}
base::CallbackListSubscription
UserEducationHelpBubbleController::AddHelpBubbleShownCallback(
base::RepeatingClosure callback) {
return help_bubble_shown_subscribers_.Add(std::move(callback));
}
void UserEducationHelpBubbleController::NotifyHelpBubbleAnchorBoundsChanged(
base::PassKey<HelpBubbleViewAsh>,
const HelpBubbleViewAsh* help_bubble_view) {
// Ignore event if the associated help bubble has not yet been shown.
if (auto it = help_bubble_metadata_by_key_.find(help_bubble_view);
it != help_bubble_metadata_by_key_.end()) {
it->second.anchor_bounds_in_screen =
GetAnchorBoundsInScreen(help_bubble_view);
help_bubble_anchor_bounds_changed_subscribers_.Notify();
}
}
void UserEducationHelpBubbleController::NotifyHelpBubbleClosed(
base::PassKey<HelpBubbleViewAsh>,
const HelpBubbleViewAsh* help_bubble_view) {
// Ignore event if the associated help bubble has not yet been shown.
if (auto it = help_bubble_metadata_by_key_.find(help_bubble_view);
it != help_bubble_metadata_by_key_.end()) {
help_bubble_metadata_by_key_.erase(it);
help_bubble_closed_subscribers_.Notify();
}
}
void UserEducationHelpBubbleController::NotifyHelpBubbleShown(
base::PassKey<HelpBubbleViewAsh>,
const HelpBubbleViewAsh* help_bubble_view) {
// Ignore event if the associated help bubble has already been shown.
if (!base::Contains(help_bubble_metadata_by_key_, help_bubble_view)) {
help_bubble_metadata_by_key_.emplace(
std::piecewise_construct, std::forward_as_tuple(help_bubble_view),
std::forward_as_tuple(help_bubble_view,
GetAnchorRootWindow(help_bubble_view),
GetAnchorBoundsInScreen(help_bubble_view)));
help_bubble_shown_subscribers_.Notify();
}
}
} // namespace ash