// 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/views/help_bubble_factory_views_ash.h"
#include <memory>
#include <optional>
#include "ash/user_education/user_education_class_properties.h"
#include "ash/user_education/user_education_util.h"
#include "ash/user_education/views/help_bubble_view_ash.h"
#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "components/user_education/common/events.h"
#include "components/user_education/common/help_bubble.h"
#include "components/user_education/common/help_bubble_params.h"
#include "components/user_education/common/user_education_class_properties.h"
#include "components/user_education/views/help_bubble_delegate.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/accessible_pane_view.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/view_utils.h"
namespace ash {
DEFINE_FRAMEWORK_SPECIFIC_METADATA(HelpBubbleViewsAsh)
DEFINE_FRAMEWORK_SPECIFIC_METADATA(HelpBubbleFactoryViewsAsh)
HelpBubbleViewsAsh::HelpBubbleViewsAsh(HelpBubbleViewAsh* help_bubble_view,
ui::TrackedElement* anchor_element)
: help_bubble_view_(help_bubble_view), anchor_element_(anchor_element) {
DCHECK(help_bubble_view);
DCHECK(help_bubble_view->GetWidget());
scoped_observation_.Observe(help_bubble_view->GetWidget());
anchor_hidden_subscription_ =
ui::ElementTracker::GetElementTracker()->AddElementHiddenCallback(
anchor_element->identifier(), anchor_element->context(),
base::BindRepeating(&HelpBubbleViewsAsh::OnElementHidden,
base::Unretained(this)));
anchor_bounds_changed_subscription_ =
ui::ElementTracker::GetElementTracker()->AddCustomEventCallback(
user_education::kHelpBubbleAnchorBoundsChangedEvent,
anchor_element->context(),
base::BindRepeating(&HelpBubbleViewsAsh::OnElementBoundsChanged,
base::Unretained(this)));
}
HelpBubbleViewsAsh::~HelpBubbleViewsAsh() {
// Needs to be called here while we still have access to HelpBubbleViews-
// specific logic.
Close();
}
bool HelpBubbleViewsAsh::ToggleFocusForAccessibility() {
// // If the bubble isn't present or can't be meaningfully focused, stop.
if (!help_bubble_view_) {
return false;
}
// If the focus isn't in the help bubble, focus the help bubble.
// Note that if is_focus_in_ancestor_widget is true, then anchor both exists
// and has a widget, so anchor->GetWidget() will always be valid.
if (!help_bubble_view_->IsFocusInHelpBubble()) {
help_bubble_view_->GetWidget()->Activate();
help_bubble_view_->RequestFocus();
return true;
}
auto* const anchor = help_bubble_view_->GetAnchorView();
if (!anchor) {
return false;
}
bool set_focus = false;
if (anchor->GetViewAccessibility().IsAccessibilityFocusable()) {
#if BUILDFLAG(IS_MAC)
// Mac does not automatically pass activation on focus, so we have to do it
// manually.
anchor->GetWidget()->Activate();
#else
// Focus the anchor. We can't request focus for an accessibility-only view
// until we turn on keyboard accessibility for its focus manager.
anchor->GetFocusManager()->SetKeyboardAccessible(true);
#endif
anchor->RequestFocus();
set_focus = true;
} else if (views::IsViewClass<views::AccessiblePaneView>(anchor)) {
// An AccessiblePaneView can receive focus, but is not necessarily itself
// accessibility focusable. Use the built-in functionality for focusing
// elements of AccessiblePaneView instead.
#if BUILDFLAG(IS_MAC)
// Mac does not automatically pass activation on focus, so we have to do it
// manually.
anchor->GetWidget()->Activate();
#else
// You can't focus an accessible pane if it's already in accessibility
// mode, so avoid doing that; the SetPaneFocus() call will go back into
// accessibility navigation mode.
anchor->GetFocusManager()->SetKeyboardAccessible(false);
#endif
set_focus =
static_cast<views::AccessiblePaneView*>(anchor)->SetPaneFocus(nullptr);
}
return set_focus;
}
void HelpBubbleViewsAsh::OnAnchorBoundsChanged() {
if (help_bubble_view_) {
static_cast<views::BubbleDialogDelegateView*>(help_bubble_view_)
->OnAnchorBoundsChanged();
}
}
gfx::Rect HelpBubbleViewsAsh::GetBoundsInScreen() const {
return help_bubble_view_
? help_bubble_view_->GetWidget()->GetWindowBoundsInScreen()
: gfx::Rect();
}
ui::ElementContext HelpBubbleViewsAsh::GetContext() const {
return help_bubble_view_
? views::ElementTrackerViews::GetContextForView(help_bubble_view_)
: ui::ElementContext();
}
bool HelpBubbleViewsAsh::AcceleratorPressed(
const ui::Accelerator& accelerator) {
if (CanHandleAccelerators()) {
ToggleFocusForAccessibility();
return true;
}
return false;
}
bool HelpBubbleViewsAsh::CanHandleAccelerators() const {
return help_bubble_view_ && help_bubble_view_->GetWidget() &&
help_bubble_view_->GetWidget()->IsActive();
}
void HelpBubbleViewsAsh::MaybeResetAnchorView() {
if (!help_bubble_view_) {
return;
}
auto* const anchor_view = help_bubble_view_->GetAnchorView();
if (!anchor_view) {
return;
}
anchor_view->SetProperty(user_education::kHasInProductHelpPromoKey, false);
}
void HelpBubbleViewsAsh::CloseBubbleImpl() {
anchor_hidden_subscription_ = base::CallbackListSubscription();
anchor_bounds_changed_subscription_ = base::CallbackListSubscription();
scoped_observation_.Reset();
MaybeResetAnchorView();
// Reset the anchor view. Closing the widget could cause callbacks which could
// theoretically destroy `this`, so
auto* const help_bubble_view = help_bubble_view_.get();
help_bubble_view_ = nullptr;
if (help_bubble_view && help_bubble_view->GetWidget()) {
help_bubble_view->GetWidget()->Close();
}
}
void HelpBubbleViewsAsh::OnWidgetDestroying(views::Widget* widget) {
Close();
}
void HelpBubbleViewsAsh::OnElementHidden(ui::TrackedElement* element) {
// There could be other elements with the same identifier as the anchor
// element, so don't close the bubble unless it is actually the anchor.
if (element != anchor_element_) {
return;
}
anchor_hidden_subscription_ = base::CallbackListSubscription();
anchor_bounds_changed_subscription_ = base::CallbackListSubscription();
anchor_element_ = nullptr;
Close();
}
void HelpBubbleViewsAsh::OnElementBoundsChanged(ui::TrackedElement* element) {
if (help_bubble_view_ && element == anchor_element_) {
OnAnchorBoundsChanged();
}
}
HelpBubbleFactoryViewsAsh::HelpBubbleFactoryViewsAsh(
const user_education::HelpBubbleDelegate* delegate)
: delegate_(delegate) {
DCHECK(delegate_);
}
HelpBubbleFactoryViewsAsh::~HelpBubbleFactoryViewsAsh() = default;
std::unique_ptr<user_education::HelpBubble>
HelpBubbleFactoryViewsAsh::CreateBubble(
ui::TrackedElement* element,
user_education::HelpBubbleParams params) {
internal::HelpBubbleAnchorParams anchor;
anchor.view = element->AsA<views::TrackedElementViews>()->view();
return CreateBubbleImpl(element, anchor, std::move(params));
}
bool HelpBubbleFactoryViewsAsh::CanBuildBubbleForTrackedElement(
const ui::TrackedElement* element) const {
return element->IsA<views::TrackedElementViews>() &&
element->AsA<views::TrackedElementViews>()->view()->GetProperty(
kHelpBubbleContextKey) == HelpBubbleContext::kAsh;
}
std::unique_ptr<user_education::HelpBubble>
HelpBubbleFactoryViewsAsh::CreateBubbleImpl(
ui::TrackedElement* element,
const internal::HelpBubbleAnchorParams& anchor,
user_education::HelpBubbleParams params) {
anchor.view->SetProperty(user_education::kHasInProductHelpPromoKey, true);
// NOTE: `HelpBubbleViewAsh` instances are owned by their widgets.
const HelpBubbleId help_bubble_id =
user_education_util::GetHelpBubbleId(params.extended_properties);
auto result = base::WrapUnique(new HelpBubbleViewsAsh(
new HelpBubbleViewAsh(help_bubble_id, anchor, std::move(params)),
element));
for (const auto& accelerator :
delegate_->GetPaneNavigationAccelerators(element)) {
result->bubble_view()->GetFocusManager()->RegisterAccelerator(
accelerator, ui::AcceleratorManager::HandlerPriority::kNormalPriority,
result.get());
}
return result;
}
} // namespace ash