blob: 0cba6e77ed7e969f8a90cf5c6897a259bf12840b [file]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include <algorithm>
#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/base/class_property.h"
#include "ui/base/default_style.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_key.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_element.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/layer_type.h"
#include "ui/display/screen.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/vector2d_conversions.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/bubble_histograms_variant.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/layout/layout_manager.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/views_features.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#include "ui/views/window/dialog_client_view.h"
#if BUILDFLAG(IS_WIN)
#include "ui/base/win/shell.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "ui/views/widget/widget_utils_mac.h"
#else
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#endif
#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif
DEFINE_UI_CLASS_PROPERTY_TYPE(std::vector<views::BubbleDialogDelegate*>*)
namespace views {
namespace {
// Some anchors may have multiple bubbles associated with them. This can happen
// if a bubble does not have close_on_deactivate and another bubble appears
// that needs the same anchor view. This property maintains a vector of bubbles
// in anchored order where the last item is the primary anchored bubble. If the
// last item is removed, the new last item, if available, is notified that it is
// the new primary anchored bubble.
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(std::vector<views::BubbleDialogDelegate*>,
kAnchorVector)
std::vector<BubbleDialogDelegate*>& GetAnchorVector(View* view) {
if (!view->GetProperty(kAnchorVector)) {
return *(view->SetProperty(
kAnchorVector, std::make_unique<std::vector<BubbleDialogDelegate*>>()));
}
return *(view->GetProperty(kAnchorVector));
}
// Override base functionality of Widget to give bubble dialogs access to the
// theme provider of the window they're anchored to.
class BubbleWidget : public Widget {
public:
BubbleWidget() = default;
BubbleWidget(const BubbleWidget&) = delete;
BubbleWidget& operator=(const BubbleWidget&) = delete;
// Widget:
const ui::ThemeProvider* GetThemeProvider() const override {
const Widget* const anchor = GetAnchorWidget();
return anchor ? anchor->GetThemeProvider() : Widget::GetThemeProvider();
}
ui::ColorProviderKey::ThemeInitializerSupplier* GetCustomTheme()
const override {
const Widget* const anchor = GetAnchorWidget();
return anchor ? anchor->GetCustomTheme() : Widget::GetCustomTheme();
}
const ui::NativeTheme* GetNativeTheme() const override {
const Widget* const anchor = GetAnchorWidget();
return anchor ? anchor->GetNativeTheme() : Widget::GetNativeTheme();
}
using Widget::GetPrimaryWindowWidget;
Widget* GetPrimaryWindowWidget() override {
Widget* const anchor = GetAnchorWidget();
return anchor ? anchor->GetPrimaryWindowWidget()
: Widget::GetPrimaryWindowWidget();
}
const ui::ColorProvider* GetColorProvider() const override {
const Widget* const primary = GetPrimaryWindowWidget();
return (primary && primary != this) ? primary->GetColorProvider()
: Widget::GetColorProvider();
}
private:
const Widget* GetAnchorWidget() const {
// TODO(pbos): Could this use Widget::parent() instead of anchor_widget()?
BubbleDialogDelegate* const bubble_delegate =
static_cast<BubbleDialogDelegate*>(widget_delegate());
return bubble_delegate ? bubble_delegate->anchor_widget() : nullptr;
}
Widget* GetAnchorWidget() {
return const_cast<Widget*>(std::as_const(*this).GetAnchorWidget());
}
};
// The frame view for bubble dialog widgets. These are not user-sizable so have
// simplified logic for minimum and maximum sizes to avoid repeated calls to
// CalculatePreferredSize().
class BubbleDialogFrameView : public BubbleFrameView {
public:
explicit BubbleDialogFrameView(const gfx::Insets& title_margins)
: BubbleFrameView(title_margins, gfx::Insets()) {}
BubbleDialogFrameView(const BubbleDialogFrameView&) = delete;
BubbleDialogFrameView& operator=(const BubbleDialogFrameView&) = delete;
// View:
gfx::Size GetMinimumSize() const override { return gfx::Size(); }
gfx::Size GetMaximumSize() const override { return gfx::Size(); }
};
// Create a widget to host the bubble.
Widget* CreateBubbleWidget(BubbleDialogDelegate* bubble,
Widget::InitParams::Ownership ownership =
Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET) {
auto bubble_widget = std::make_unique<BubbleWidget>();
Widget::InitParams bubble_params(ownership, Widget::InitParams::TYPE_BUBBLE);
bubble_params.delegate = bubble;
bubble_params.opacity = Widget::InitParams::WindowOpacity::kTranslucent;
bubble_params.accept_events = bubble->accept_events();
bubble_params.accept_drag_drop = bubble->accept_drag_drop();
bubble_params.remove_standard_frame = true;
bubble_params.layer_type = bubble->layer_type();
// TODO(crbug.com/41493925): Remove CHECK once native frame dialogs support
// autosize.
CHECK(!bubble->is_autosized() || bubble->use_custom_frame())
<< "Autosizing native frame dialogs is not supported.";
bubble_params.autosize = bubble->is_autosized();
// Use a window default shadow if the bubble doesn't provides its own.
if (bubble->GetShadow() == BubbleBorder::NO_SHADOW) {
bubble_params.shadow_type = Widget::InitParams::ShadowType::kDefault;
} else {
bubble_params.shadow_type = Widget::InitParams::ShadowType::kNone;
}
gfx::NativeView parent = gfx::NativeView();
if (bubble->has_parent()) {
if (bubble->parent_window()) {
parent = bubble->parent_window();
} else if (bubble->anchor_widget()) {
parent = bubble->anchor_widget()->GetNativeView();
}
}
bubble_params.parent = parent;
bubble_params.activatable = bubble->CanActivate()
? Widget::InitParams::Activatable::kYes
: Widget::InitParams::Activatable::kNo;
bubble->OnBeforeBubbleWidgetInit(&bubble_params, bubble_widget.get());
DCHECK(bubble_params.parent || !bubble->has_parent());
bubble_widget->Init(std::move(bubble_params));
return bubble_widget.release();
}
} // namespace
class BubbleDialogDelegate::AnchorViewObserver : public ViewObserver {
public:
AnchorViewObserver(BubbleDialogDelegate* parent, View* anchor_view)
: parent_(parent), anchor_view_(anchor_view) {
view_observation_.Observe(anchor_view);
scoped_notify_ = std::make_unique<
views::View::ScopedNotifyObserversOnVisibleBoundsChanged>(*anchor_view);
AddToAnchorVector();
}
AnchorViewObserver(const AnchorViewObserver&) = delete;
AnchorViewObserver& operator=(const AnchorViewObserver&) = delete;
~AnchorViewObserver() override {
RemoveFromAnchorVector();
}
View* anchor_view() const { return anchor_view_; }
void UpdateBubbleVisibility(bool visible) {
bubble_widget_visible_ = visible;
if (visible && update_bounds_when_visible_) {
parent_->OnAnchorBoundsChanged();
update_bounds_when_visible_ = false;
}
}
// ViewObserver:
void OnViewIsDeleting(View* observed_view) override {
// The anchor is being deleted, make sure the parent bubble no longer
// observes it.
DCHECK_EQ(anchor_view_, observed_view);
scoped_notify_ = nullptr;
parent_->SetAnchorView(nullptr);
}
void OnViewVisibleBoundsChanged(View* observed_view) override {
DCHECK_EQ(anchor_view_, observed_view);
if (bubble_widget_visible_) {
parent_->OnAnchorBoundsChanged();
} else {
update_bounds_when_visible_ = true;
}
}
// TODO(pbos): Consider observing View visibility changes and only updating
// view bounds when the anchor is visible.
private:
void AddToAnchorVector() {
auto& vector = GetAnchorVector(anchor_view_);
DCHECK(!std::ranges::contains(vector, parent_));
vector.push_back(parent_);
}
void RemoveFromAnchorVector() {
auto& vector = GetAnchorVector(anchor_view_);
DCHECK(!vector.empty());
auto iter = vector.cend() - 1;
bool latest_anchor_bubble_will_change = (*iter == parent_);
if (!latest_anchor_bubble_will_change) {
iter = std::find(vector.cbegin(), iter, parent_);
}
DCHECK(iter != vector.cend());
vector.erase(iter);
if (!vector.empty() && latest_anchor_bubble_will_change) {
vector.back()->NotifyAnchoredBubbleIsPrimary();
}
}
std::unique_ptr<views::View::ScopedNotifyObserversOnVisibleBoundsChanged>
scoped_notify_;
const raw_ptr<BubbleDialogDelegate> parent_;
const raw_ptr<View> anchor_view_;
bool bubble_widget_visible_ = true;
bool update_bounds_when_visible_ = false;
base::ScopedObservation<views::View, views::ViewObserver> view_observation_{
this};
};
// This class is responsible for observing events on a BubbleDialogDelegate's
// anchor widget and notifying the BubbleDialogDelegate of them.
#if BUILDFLAG(IS_MAC)
class BubbleDialogDelegate::AnchorWidgetObserver : public WidgetObserver {
#else
class BubbleDialogDelegate::AnchorWidgetObserver : public WidgetObserver,
public aura::WindowObserver {
#endif
public:
AnchorWidgetObserver(BubbleDialogDelegate* owner, Widget* widget)
: owner_(owner) {
widget_observation_.Observe(widget);
#if !BUILDFLAG(IS_MAC)
window_observation_.Observe(widget->GetNativeWindow());
#endif
}
~AnchorWidgetObserver() override = default;
// WidgetObserver:
void OnWidgetDestroying(Widget* widget) override {
#if !BUILDFLAG(IS_MAC)
DCHECK(window_observation_.IsObservingSource(widget->GetNativeWindow()));
window_observation_.Reset();
#endif
DCHECK(widget_observation_.IsObservingSource(widget));
widget_observation_.Reset();
owner_->OnAnchorWidgetDestroying();
// |this| may be destroyed here!
}
void OnWidgetActivationChanged(Widget* widget, bool active) override {
owner_->OnWidgetActivationChanged(widget, active);
}
void OnWidgetBoundsChanged(Widget* widget, const gfx::Rect&) override {
owner_->OnAnchorBoundsChanged();
}
void OnWidgetThemeChanged(Widget* widget) override {
// TODO(dfried): Consider merging BubbleWidget with ThemeCopyingWidget
// instead of observing the theme here.
owner_->GetWidget()->ThemeChanged();
}
#if !BUILDFLAG(IS_MAC)
// aura::WindowObserver:
void OnWindowTransformed(aura::Window* window,
ui::PropertyChangeReason reason) override {
if (window->is_destroying()) {
return;
}
// Update the anchor bounds when the transform animation is complete, or
// when the transform is set without animation.
if (!window->layer()->GetAnimator()->IsAnimatingOnePropertyOf(
ui::LayerAnimationElement::TRANSFORM)) {
owner_->OnAnchorBoundsChanged();
}
}
// If the native window is closed by the OS, OnWidgetDestroying() won't
// fire. Instead, OnWindowDestroying() will fire before aura::Window
// destruction. See //docs/ui/views/widget_destruction.md.
void OnWindowDestroying(aura::Window* window) override {
window_observation_.Reset();
}
#endif
private:
raw_ptr<BubbleDialogDelegate> owner_;
base::ScopedObservation<views::Widget, views::WidgetObserver>
widget_observation_{this};
#if !BUILDFLAG(IS_MAC)
base::ScopedObservation<aura::Window, aura::WindowObserver>
window_observation_{this};
#endif
};
// This class is responsible for observing events on a BubbleDialogDelegate's
// widget and notifying the BubbleDialogDelegate of them.
class BubbleDialogDelegate::BubbleWidgetObserver : public WidgetObserver {
public:
BubbleWidgetObserver(BubbleDialogDelegate* owner, Widget* widget)
: owner_(owner) {
observation_.Observe(widget);
}
~BubbleWidgetObserver() override = default;
void OnWidgetClosing(Widget* widget) override {
owner_->OnBubbleWidgetClosing();
owner_->OnWidgetClosing(widget);
}
void OnWidgetDestroying(Widget* widget) override {
owner_->OnWidgetDestroying(widget);
}
void OnWidgetDestroyed(Widget* widget) override {
DCHECK(observation_.IsObservingSource(widget));
observation_.Reset();
owner_->OnWidgetDestroyed(widget);
}
void OnWidgetBoundsChanged(Widget* widget, const gfx::Rect& bounds) override {
owner_->OnWidgetBoundsChanged(widget, bounds);
}
void OnWidgetVisibilityChanged(Widget* widget, bool visible) override {
owner_->OnBubbleWidgetVisibilityChanged(visible);
owner_->OnWidgetVisibilityChanged(widget, visible);
}
void OnWidgetActivationChanged(Widget* widget, bool active) override {
owner_->OnBubbleWidgetActivationChanged(active);
owner_->OnWidgetActivationChanged(widget, active);
}
private:
const raw_ptr<BubbleDialogDelegate> owner_;
base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
this};
};
class BubbleDialogDelegateView::CloseOnDeactivatePin::Pins {
public:
Pins() = default;
~Pins() = default;
bool is_pinned() const { return !pins_.empty(); }
base::WeakPtr<Pins> GetWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); }
void AddPin(CloseOnDeactivatePin* pin) {
const auto result = pins_.insert(pin);
DCHECK(result.second);
}
void RemovePin(CloseOnDeactivatePin* pin) {
const auto result = pins_.erase(pin);
DCHECK(result);
}
protected:
std::set<raw_ptr<CloseOnDeactivatePin, SetExperimental>> pins_;
base::WeakPtrFactory<Pins> weak_ptr_factory_{this};
};
BubbleDialogDelegate::CloseOnDeactivatePin::CloseOnDeactivatePin(
base::WeakPtr<Pins> pins)
: pins_(pins) {
pins_->AddPin(this);
}
BubbleDialogDelegate::CloseOnDeactivatePin::~CloseOnDeactivatePin() {
Pins* const pins = pins_.get();
if (pins) {
pins->RemovePin(this);
}
}
BubbleDialogDelegate::BubbleDialogDelegate(BubbleAnchor anchor,
BubbleBorder::Arrow arrow,
BubbleBorder::Shadow shadow,
bool autosize)
: arrow_(arrow),
shadow_config_({.shadow_type = shadow}),
adjust_if_offscreen_(PlatformStyle::kAdjustBubbleIfOffscreen),
autosize_(autosize),
close_on_deactivate_pins_(std::make_unique<CloseOnDeactivatePin::Pins>()),
bubble_created_time_(base::TimeTicks::Now()) {
bubble_uma_logger().set_delegate(this);
SetAnchor(anchor);
SetArrow(arrow);
SetShowCloseButton(false);
LayoutProvider* const layout_provider = LayoutProvider::Get();
set_frame_margins(
{.contents = layout_provider->GetDialogInsetsForContentType(
DialogContentType::kText, DialogContentType::kText),
.title = layout_provider->GetInsetsMetric(INSETS_DIALOG_TITLE),
.footnote = layout_provider->GetInsetsMetric(INSETS_DIALOG_FOOTNOTE)});
set_desired_bounds_delegate(base::BindRepeating(
&BubbleDialogDelegate::GetDesiredBubbleBounds, base::Unretained(this)));
RegisterWidgetInitializedCallback(base::BindOnce(
[](BubbleDialogDelegate* bubble_delegate) {
// Call the theme callback to make sure the initial theme is picked up
// by the BubbleDialogDelegate.
bubble_delegate->UpdateFrameColor();
},
this));
// Bind a callback to the compositor for logging time from bubble creation to
// successful presentation of the next frame.
RegisterWidgetInitializedCallback(base::BindOnce(
[](BubbleDialogDelegate* delegate, base::TimeTicks bubble_created_time) {
delegate->GetWidget()
->GetCompositor()
->RequestSuccessfulPresentationTimeForNextFrame(base::BindOnce(
[](base::WeakPtr<BubbleDialogDelegate::BubbleUmaLogger>
uma_logger,
base::TimeTicks bubble_created_time,
const viz::FrameTimingDetails& frame_timing_details) {
base::TimeTicks presentation_timestamp =
frame_timing_details.presentation_feedback.timestamp;
if (!uma_logger) {
return;
}
uma_logger->LogMetric(
base::UmaHistogramTimes, "CreateToPresentationTime",
presentation_timestamp - bubble_created_time);
},
delegate->bubble_uma_logger().GetWeakPtr(),
bubble_created_time));
},
base::Unretained(this), *bubble_created_time_));
}
BubbleDialogDelegate::BubbleDialogDelegate(View* anchor_view,
BubbleBorder::Arrow arrow,
BubbleBorder::Shadow shadow,
bool autosize)
: BubbleDialogDelegate(BubbleAnchor(anchor_view), arrow, shadow, autosize) {
}
BubbleDialogDelegate::~BubbleDialogDelegate() {
SetAnchorView(nullptr);
}
// static
Widget* BubbleDialogDelegate::CreateBubbleInternal(
BubbleDialogDelegate* delegate,
Widget::InitParams::Ownership ownership,
Widget::ClosedCallback on_close) {
// On Mac, ModalType::kWindow is implemented using sheets, which can't be
// anchored at a specific point - they are always placed near the top center
// of the window. To avoid unpleasant surprises, disallow setting an anchor
// view or rectangle on these types of bubbles.
if (delegate->GetModalType() == ui::mojom::ModalType::kWindow) {
DCHECK(!delegate->GetAnchorView());
DCHECK_EQ(delegate->GetAnchorRect(), gfx::Rect());
}
delegate->Init();
// Get the latest anchor widget from the anchor view at bubble creation time.
if (auto* anchor_view = delegate->GetAnchorView()) {
delegate->SetAnchorView(anchor_view);
}
Widget* const bubble_widget = CreateBubbleWidget(delegate, ownership);
delegate->SizeToContents();
delegate->bubble_widget_observer_ =
std::make_unique<BubbleWidgetObserver>(delegate, bubble_widget);
if (on_close) {
bubble_widget->MakeCloseSynchronous(std::move(on_close));
}
return bubble_widget;
}
// static
Widget* BubbleDialogDelegate::CreateBubbleDeprecated(
BubbleDialogDelegate* bubble_delegate,
Widget::InitParams::Ownership ownership) {
return CreateBubbleInternal(bubble_delegate, ownership);
}
// static
Widget* BubbleDialogDelegate::CreateBubbleDeprecated(
std::unique_ptr<BubbleDialogDelegate> bubble_delegate_unique,
Widget::InitParams::Ownership ownership) {
return CreateBubbleDeprecated(bubble_delegate_unique.release(), ownership);
}
// static
std::unique_ptr<Widget> BubbleDialogDelegate::CreateBubble(
BubbleDialogDelegate* delegate,
base::OnceCallback<void(Widget::ClosedReason)> on_close) {
return base::WrapUnique(CreateBubbleInternal(
delegate, Widget::InitParams::CLIENT_OWNS_WIDGET, std::move(on_close)));
}
// static
Widget* BubbleDialogDelegateView::CreateBubble(
BubbleDialogDelegateView* delegate_view,
Widget::InitParams::Ownership ownership) {
return CreateBubble(base::WrapUnique(delegate_view), ownership);
}
BubbleDialogDelegateView::BubbleDialogDelegateView(BubbleAnchor anchor,
BubbleBorder::Arrow arrow,
BubbleBorder::Shadow shadow,
bool autosize)
: BubbleDialogDelegate(anchor, arrow, shadow, autosize) {
bubble_uma_logger().set_bubble_view(this);
}
BubbleDialogDelegateView::~BubbleDialogDelegateView() {
// TODO(pbos): Investigate if this is actually still needed, and if so
// document here why that's the case. If it's due to specific client layout
// managers, push this down to client destructors.
SetLayoutManager(nullptr);
// TODO(pbos): See if we can resolve this better. This currently prevents a
// crash that shows up in BubbleFrameViewTest.WidthSnaps. This crash seems to
// happen at GetWidget()->IsVisible() inside SetAnchorView(nullptr) and seems
// to be as a result of WidgetDelegate's widget_ not getting updated during
// destruction when BubbleDialogDelegateView::DeleteDelegate() doesn't delete
// itself, as Widget drops a reference to widget_delegate_ and can't inform it
// of WidgetDeleting in ~Widget.
SetAnchorView(nullptr);
}
BubbleDialogDelegate* BubbleDialogDelegate::AsBubbleDialogDelegate() {
return this;
}
std::unique_ptr<FrameView> BubbleDialogDelegate::CreateFrameView(
Widget* widget) {
const FrameMargins& margin = frame_margins();
auto frame = std::make_unique<BubbleDialogFrameView>(margin.title);
frame->SetFootnoteMargins(margin.footnote);
frame->SetFootnoteView(DisownFootnoteView());
frame->set_use_anchor_window_bounds(use_anchor_window_bounds_);
if (available_screen_bounds_callback_) {
frame->set_available_screen_bounds_callback(
available_screen_bounds_callback_);
}
std::unique_ptr<BubbleBorder> border =
std::make_unique<BubbleBorder>(arrow(), GetShadow());
border->set_background_color(background_color());
if (shadow_config_.elevation.has_value()) {
border->set_md_shadow_elevation(shadow_config_.elevation.value());
}
border->set_avoid_shadow_overlap(shadow_config_.avoid_overlap_shadow);
if (border_style_.insets.has_value()) {
border->set_insets(border_style_.insets.value());
}
border->set_draw_border_stroke(border_style_.draw_border_stroke);
if (GetParams().round_corners) {
border->set_rounded_corners(gfx::RoundedCornersF(GetCornerRadius()));
}
frame->SetBubbleBorder(std::move(border));
return frame;
}
ClientView* BubbleDialogDelegate::CreateClientView(Widget* widget) {
auto* client_view = DialogDelegate::CreateClientView(widget);
// In order for the `client_view`'s content view hierarchy to respect its
// rounded corner clip we must paint the client view to a layer. This is
// necessary because layers do not respect the clip of a non-layer backed
// parent.
client_view->SetPaintToLayer(layer_type());
client_view->layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF(GetCornerRadius()));
client_view->layer()->SetIsFastRoundedCorner(true);
return client_view;
}
Widget* BubbleDialogDelegateView::GetWidget() {
return View::GetWidget();
}
const Widget* BubbleDialogDelegateView::GetWidget() const {
return View::GetWidget();
}
View* BubbleDialogDelegateView::GetContentsView() {
return this;
}
void BubbleDialogDelegate::OnBubbleWidgetClosing() {
// To prevent keyboard focus traversal issues, the anchor view's
// kAnchoredDialogKey property is cleared immediately upon Close(). This
// avoids a bug that occured when a focused anchor view is made unfocusable
// right after the bubble is closed. Previously, focus would advance into the
// bubble then would be lost when the bubble was destroyed.
//
// If kAnchoredDialogKey does not point to |this|, then |this| is not on the
// focus traversal path. Don't reset kAnchoredDialogKey or we risk detaching
// a widget from the traversal path.
if (GetAnchorView() &&
GetAnchorView()->GetProperty(kAnchoredDialogKey) == this) {
GetAnchorView()->ClearProperty(kAnchoredDialogKey);
}
if (bubble_shown_time_.has_value()) {
bubble_shown_duration_ += base::TimeTicks::Now() - *bubble_shown_time_;
bubble_shown_time_.reset();
}
bubble_uma_logger().LogMetric(base::UmaHistogramLongTimes, "TimeVisible",
bubble_shown_duration_);
bubble_uma_logger().LogMetric(base::UmaHistogramEnumeration, "CloseReason",
GetWidget()->closed_reason());
}
void BubbleDialogDelegate::OnAnchorWidgetDestroying() {
anchor_tracked_element_ = nullptr;
SetAnchorView(nullptr);
}
void BubbleDialogDelegate::OnBubbleWidgetActivationChanged(bool active) {
#if BUILDFLAG(IS_MAC)
// Install |mac_bubble_closer_| the first time the widget becomes active.
if (active && !mac_bubble_closer_) {
mac_bubble_closer_ = std::make_unique<ui::BubbleCloser>(
GetWidget()->GetNativeWindow(),
base::BindRepeating(&BubbleDialogDelegate::OnDeactivate,
base::Unretained(this)));
}
#endif
if (!active) {
OnDeactivate();
}
}
void BubbleDialogDelegate::OnAnchorWidgetBoundsChanged() {
if (GetBubbleFrameView()) {
SizeToContents();
}
}
BubbleBorder::Shadow BubbleDialogDelegate::GetShadow() const {
if (!Widget::IsWindowCompositingSupported()) {
return BubbleBorder::Shadow::NO_SHADOW;
}
return shadow_config_.shadow_type;
}
View* BubbleDialogDelegate::GetAnchorView() const {
if (!anchor_view_observer_) {
return nullptr;
}
return anchor_view_observer_->anchor_view();
}
void BubbleDialogDelegate::SetMainImage(ui::ImageModel main_image) {
// Adding a main image while the bubble is showing is not supported (but
// changing it is). Adding an image while it's showing would require a jarring
// re-layout.
if (main_image_.IsEmpty()) {
DCHECK(!GetBubbleFrameView());
}
main_image_ = std::move(main_image);
if (GetBubbleFrameView()) {
GetBubbleFrameView()->UpdateMainImage();
}
}
bool BubbleDialogDelegate::ShouldCloseOnDeactivate() const {
return close_on_deactivate_ && !close_on_deactivate_pins_->is_pinned();
}
std::unique_ptr<BubbleDialogDelegate::CloseOnDeactivatePin>
BubbleDialogDelegate::PreventCloseOnDeactivate() {
return base::WrapUnique(
new CloseOnDeactivatePin(close_on_deactivate_pins_->GetWeakPtr()));
}
void BubbleDialogDelegate::SetHighlightedElement(ui::ElementIdentifier id) {
auto* element_tracker = ui::ElementTracker::GetElementTracker();
auto* parent_widget =
anchor_widget() ? anchor_widget()
: views::Widget::GetWidgetForNativeView(parent_window());
auto context = views::ElementTrackerViews::GetContextForWidget(parent_widget);
// `highlighted_button_tracker_` will deal with the hiding part.
// Unretained is safe because `highlighted_element_shown_subscription_` owns
// the callback subscription.
highlighted_element_shown_subscription_ =
element_tracker->AddElementShownCallback(
id, context,
base::BindRepeating(
&BubbleDialogDelegate::SetResolvedHighlightedElement,
base::Unretained(this)));
SetResolvedHighlightedElement(element_tracker->GetUniqueElement(id, context));
}
void BubbleDialogDelegate::SetResolvedHighlightedElement(
ui::TrackedElement* highlighted_element) {
bool visible = GetWidget() && GetWidget()->IsVisible();
highlighted_element_tracker_ = highlighted_element;
if (visible) {
UpdateHighlightedButton(true);
}
}
void BubbleDialogDelegate::SetBackgroundColor(ui::ColorVariant color) {
if (color_ == color) {
return;
}
color_ = color;
if (GetWidget()) {
UpdateFrameColor();
}
}
void BubbleDialogDelegate::SetArrow(BubbleBorder::Arrow arrow) {
SetArrowWithoutResizing(arrow);
// If SetArrow() is called before CreateWidget(), there's no need to update
// the BubbleFrameView.
if (GetBubbleFrameView()) {
SizeToContents();
}
}
void BubbleDialogDelegate::SetArrowWithoutResizing(BubbleBorder::Arrow arrow) {
if (base::i18n::IsRTL()) {
arrow = BubbleBorder::horizontal_mirror(arrow);
}
if (arrow_ == arrow) {
return;
}
arrow_ = arrow;
// If SetArrow() is called before CreateWidget(), there's no need to update
// the BubbleFrameView.
if (GetBubbleFrameView()) {
GetBubbleFrameView()->SetArrow(arrow);
}
}
gfx::Rect BubbleDialogDelegate::GetAnchorRect() const {
// TODO(tluk) eliminate the need for GetAnchorRect() to return an empty rect
// if neither an |anchor_rect_| or an anchor view have been set.
View* anchor_view = GetAnchorView();
if (anchor_view) {
anchor_rect_ = anchor_view->GetAnchorBoundsInScreen();
} else if (anchor_tracked_element_) {
anchor_rect_ = anchor_tracked_element_.get()->GetScreenBounds();
} else {
return anchor_rect_.value_or(gfx::Rect());
}
#if !BUILDFLAG(IS_MAC)
// GetAnchorBoundsInScreen returns values that take anchor widget's
// translation into account, so undo that here. Without this, features which
// apply transforms on windows such as ChromeOS overview mode will see bubbles
// offset.
if (anchor_widget_) {
gfx::Transform transform =
anchor_widget_->GetNativeWindow()->layer()->GetTargetTransform();
if (!transform.IsIdentity()) {
anchor_rect_->Offset(
-gfx::ToRoundedVector2d(transform.To2dTranslation()));
}
}
#endif
// Remove additional whitespace padding that was added to the view
// so that anchor_rect centers on the anchor and not skewed by the whitespace
BubbleFrameView* frame_view = GetBubbleFrameView();
if (anchor_view && frame_view && frame_view->GetDisplayVisibleArrow()) {
gfx::Insets* padding = anchor_view->GetProperty(kInternalPaddingKey);
if (padding != nullptr) {
anchor_rect_->Inset(*padding);
}
}
return anchor_rect_.value();
}
void BubbleDialogDelegate::UseCompactMargins() {
set_frame_margins({.contents = gfx::Insets(6)});
}
// static
gfx::Size BubbleDialogDelegate::GetMaxAvailableScreenSpaceToPlaceBubble(
BubbleAnchor anchor,
BubbleBorder::Arrow arrow,
bool adjust_if_offscreen,
BubbleFrameView::PreferredArrowAdjustment arrow_adjustment) {
// TODO(sanchit.abrol@microsoft.com): Implement for other arrows.
DCHECK(arrow == BubbleBorder::TOP_LEFT || arrow == BubbleBorder::TOP_RIGHT ||
arrow == BubbleBorder::BOTTOM_RIGHT ||
arrow == BubbleBorder::BOTTOM_LEFT);
DCHECK_EQ(arrow_adjustment,
BubbleFrameView::PreferredArrowAdjustment::kMirror);
#if BUILDFLAG(IS_OZONE)
// This function should not be called in ozone platforms where global screen
// coordinates are not available.
DCHECK(ui::OzonePlatform::GetInstance()
->GetPlatformProperties()
.supports_global_screen_coordinates);
#endif
gfx::Rect anchor_rect;
if (View* view = anchor.GetIfView()) {
anchor_rect = view->GetAnchorBoundsInScreen();
} else {
ui::TrackedElement* element = anchor.GetIfElement();
CHECK(element);
anchor_rect = element->GetScreenBounds();
}
gfx::Rect screen_rect =
display::Screen::Get()
->GetDisplayNearestPoint(anchor_rect.CenterPoint())
.work_area();
gfx::Size max_available_space;
if (adjust_if_offscreen) {
max_available_space = GetAvailableSpaceToPlaceBubble(
BubbleBorder::TOP_LEFT, anchor_rect, screen_rect);
max_available_space.SetToMax(GetAvailableSpaceToPlaceBubble(
BubbleBorder::TOP_RIGHT, anchor_rect, screen_rect));
max_available_space.SetToMax(GetAvailableSpaceToPlaceBubble(
BubbleBorder::BOTTOM_RIGHT, anchor_rect, screen_rect));
max_available_space.SetToMax(GetAvailableSpaceToPlaceBubble(
BubbleBorder::BOTTOM_LEFT, anchor_rect, screen_rect));
} else {
max_available_space =
GetAvailableSpaceToPlaceBubble(arrow, anchor_rect, screen_rect);
}
return max_available_space;
}
// static
gfx::Size BubbleDialogDelegate::GetAvailableSpaceToPlaceBubble(
BubbleBorder::Arrow arrow,
gfx::Rect anchor_rect,
gfx::Rect screen_rect) {
int available_height_below = screen_rect.bottom() - anchor_rect.bottom();
int available_height_above = anchor_rect.y() - screen_rect.y();
int available_width_on_left = anchor_rect.right() - screen_rect.x();
int available_width_on_right = screen_rect.right() - anchor_rect.x();
return {BubbleBorder::is_arrow_on_left(arrow) ? available_width_on_right
: available_width_on_left,
BubbleBorder::is_arrow_on_top(arrow) ? available_height_below
: available_height_above};
}
void BubbleDialogDelegate::OnAnchorBoundsChanged() {
if (!GetWidget() || !GetBubbleFrameView()) {
return;
}
// TODO(pbos): Reconsider whether to update the anchor when the view isn't
// drawn.
SizeToContents();
// We will not accept input event a short time after anchored view changed.
UpdateInputProtectorsTimeStamp();
}
void BubbleDialogDelegate::UpdateInputProtectorsTimeStamp() {
if (auto* dialog = GetDialogClientView()) {
dialog->UpdateInputProtectorTimeStamp();
}
GetBubbleFrameView()->UpdateInputProtectorTimeStamp();
}
BubbleDialogDelegate::BubbleUmaLogger::BubbleUmaLogger() = default;
BubbleDialogDelegate::BubbleUmaLogger::~BubbleUmaLogger() = default;
base::WeakPtr<BubbleDialogDelegate::BubbleUmaLogger>
BubbleDialogDelegate::BubbleUmaLogger::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
std::optional<std::string>
BubbleDialogDelegate::BubbleUmaLogger::GetBubbleName() const {
// Some dialogs might only use BDD and not BDDV. In those cases, the class
// name should be based on BDDs' content view.
if (delegate_.has_value()) {
std::string class_name(
delegate_.value()->GetContentsView()->GetClassName());
if (class_name != "View") {
return class_name;
}
}
if (bubble_view_.has_value()) {
return std::string(bubble_view_.value()->GetClassName());
}
return std::nullopt;
}
template <typename Value>
void BubbleDialogDelegate::BubbleUmaLogger::LogMetric(
void (*uma_func)(const std::string&, Value),
std::string_view histogram_name,
Value value) const {
if (!base::FeatureList::IsEnabled(::features::kBubbleMetricsApi)) {
return;
}
// Record histogram for all BDDV subclasses under a generic name
uma_func(base::StrCat({"Bubble.All.", histogram_name}), value);
// Record histograms for specific BDDV subclasses
std::optional<std::string> bubble_name = GetBubbleName();
if (!bubble_name.has_value()) {
return;
}
if (allowed_class_names_for_testing_.has_value()) {
if (!std::ranges::contains(allowed_class_names_for_testing_.value(),
bubble_name.value())) {
return;
}
} else if (!views_metrics::IsValidBubbleName(bubble_name.value())) {
return;
}
uma_func(base::StrCat({"Bubble.", bubble_name.value(), ".", histogram_name}),
value);
}
// Instantiate template function to be able to use in views_unittests.
template VIEWS_EXPORT void BubbleDialogDelegate::BubbleUmaLogger::LogMetric<
base::TimeDelta>(void (*uma_func)(const std::string&, base::TimeDelta),
std::string_view histogram_name,
base::TimeDelta value) const;
gfx::Rect BubbleDialogDelegate::GetBubbleBounds() {
// The argument rect has its origin at the bubble's arrow anchor point;
// its size is the preferred size of the bubble's client view (this view).
bool anchor_minimized = anchor_widget() && anchor_widget()->IsMinimized();
// If GetAnchorView() returns nullptr or GetAnchorRect() returns an empty rect
// at (0, 0), don't try and adjust arrow if off-screen.
gfx::Rect anchor_rect = GetAnchorRect();
bool has_anchor = GetAnchorView() || anchor_rect != gfx::Rect();
bool adjust_to_fix_available_bounds =
adjust_if_offscreen_ && !anchor_minimized && has_anchor;
// Some platforms, such as ozone/wayland, do not support global screen
// coordinates manipulation by client applications, which makes bubble
// offscreen adjustments buggy, so bypass it if that's the case.
#if BUILDFLAG(IS_OZONE)
if (!ui::OzonePlatform::GetInstance()
->GetPlatformProperties()
.supports_global_screen_coordinates) {
adjust_to_fix_available_bounds = false;
}
#endif
return GetBubbleFrameView()->GetUpdatedWindowBounds(
anchor_rect, arrow(), GetWidget()->client_view()->GetPreferredSize({}),
adjust_to_fix_available_bounds);
}
ax::mojom::Role BubbleDialogDelegate::GetAccessibleWindowRole() {
const ax::mojom::Role accessible_role =
WidgetDelegate::GetAccessibleWindowRole();
// If the accessible role has been explicitly set to anything else than its
// default, use it. The rest translates kWindow to kDialog or kAlertDialog.
if (accessible_role != ax::mojom::Role::kWindow) {
return accessible_role;
}
// If something in the dialog has initial focus, use the dialog role.
// Screen readers understand what to announce when focus moves within one.
if (GetInitiallyFocusedView()) {
return ax::mojom::Role::kDialog;
}
// Otherwise, return |ax::mojom::Role::kAlertDialog| which will make screen
// readers announce the contents of the bubble dialog as soon as it appears,
// as long as we also fire |ax::mojom::Event::kAlert|.
return ax::mojom::Role::kAlertDialog;
}
gfx::Rect BubbleDialogDelegate::GetDesiredBubbleBounds() {
CHECK(use_custom_frame())
<< "GetBubbleBounds() for native frame dialogs is not supported.";
gfx::Rect bubble_bounds = GetBubbleBounds();
#if BUILDFLAG(IS_MAC)
// GetBubbleBounds() doesn't take the Mac NativeWindow's style mask into
// account, so we need to adjust the size.
gfx::Size actual_size =
GetWindowSizeForClientSize(GetWidget(), bubble_bounds.size());
bubble_bounds.set_size(actual_size);
#endif
return bubble_bounds;
}
gfx::Size BubbleDialogDelegateView::GetMinimumSize() const {
// Note that although BubbleDialogFrameView will never invoke this, a subclass
// may override CreateFrameView() to provide a FrameView
// that does. See http://crbug.com/844359.
return gfx::Size();
}
gfx::Size BubbleDialogDelegateView::GetMaximumSize() const {
return gfx::Size();
}
void BubbleDialogDelegate::SetAnchorWidget(views::Widget* anchor_widget) {
if (anchor_widget_ == anchor_widget) {
return;
}
// Reset the anchor view.
SetAnchorView(nullptr);
if (anchor_widget_) {
anchor_widget_observer_.reset();
}
anchor_widget_ = anchor_widget;
if (anchor_widget_) {
anchor_widget_observer_ =
std::make_unique<AnchorWidgetObserver>(this, anchor_widget_);
}
}
void BubbleDialogDelegate::SetAnchorView(View* anchor_view) {
if (anchor_view) {
anchor_tracked_element_ = nullptr;
}
if (anchor_view && anchor_view->GetWidget()) {
anchor_widget_observer_ =
std::make_unique<AnchorWidgetObserver>(this, anchor_view->GetWidget());
} else {
anchor_widget_observer_.reset();
}
if (GetAnchorView()) {
if (GetAnchorView()->GetProperty(kAnchoredDialogKey) == this) {
GetAnchorView()->ClearProperty(kAnchoredDialogKey);
}
anchor_view_observer_.reset();
}
// When the anchor view gets set the associated anchor widget might
// change as well.
if (!anchor_view || anchor_widget() != anchor_view->GetWidget()) {
if (anchor_widget()) {
anchor_widget_ = nullptr;
}
if (anchor_view) {
anchor_widget_ = anchor_view->GetProperty(kWidgetForAnchoringKey);
if (!anchor_widget_) {
anchor_widget_ = anchor_view->GetWidget();
}
}
}
if (anchor_view) {
anchor_view_observer_ =
std::make_unique<AnchorViewObserver>(this, anchor_view);
// Do not update anchoring for NULL views; this could indicate
// that our NativeWindow is being destroyed, so it would be
// dangerous for us to update our anchor bounds at that
// point. (It's safe to skip this, since if we were to update the
// bounds when |anchor_view| is NULL, the bubble won't move.)
OnAnchorBoundsChanged();
SetAnchoredDialogKey();
}
const bool visible = GetWidget() && GetWidget()->IsVisible();
UpdateHighlightedButton(visible);
}
void BubbleDialogDelegate::SetAnchorRect(const gfx::Rect& rect) {
anchor_rect_ = rect;
if (GetWidget()) {
OnAnchorBoundsChanged();
}
}
void BubbleDialogDelegate::SetAnchor(BubbleAnchor anchor) {
if (View* view = anchor.GetIfView()) {
SetAnchorView(view);
} else if (ui::TrackedElement* tracked_element = anchor.GetIfElement()) {
if (auto* element_views = tracked_element->AsA<TrackedElementViews>()) {
SetAnchorView(element_views->view());
} else {
anchor_tracked_element_ = tracked_element;
SetAnchorView(nullptr);
SetAnchorRect(tracked_element->GetScreenBounds());
Widget* widget =
Widget::GetWidgetForNativeView(tracked_element->GetNativeView());
CHECK(widget) << "BubbleAnchor only supports TrackedElement backed by a "
<< "Widget.";
SetAnchorWidget(widget);
}
} else {
CHECK(anchor.IsNull());
anchor_tracked_element_ = nullptr;
SetAnchorView(nullptr);
SetAnchorRect(gfx::Rect());
}
}
BubbleAnchor BubbleDialogDelegate::GetAnchor() const {
if (GetAnchorView()) {
return BubbleAnchor(GetAnchorView());
}
if (anchor_tracked_element_) {
return BubbleAnchor(anchor_tracked_element_.get());
}
return BubbleAnchor();
}
bool BubbleDialogDelegate::IsSameAnchor(BubbleAnchor anchor) const {
// Normalize `anchor` similar to how SetAnchor would.
if (ui::TrackedElement* tracked_element = anchor.GetIfElement()) {
if (auto* element_views = tracked_element->AsA<TrackedElementViews>()) {
anchor = BubbleAnchor(element_views->view());
}
}
BubbleAnchor lhs = GetAnchor();
if (ui::TrackedElement* rhs_element = anchor.GetIfElement()) {
return rhs_element == lhs.GetIfElement();
} else if (views::View* rhs_view = anchor.GetIfView()) {
return rhs_view == lhs.GetIfView();
} else {
DCHECK(anchor.IsNull());
return lhs.IsNull();
}
}
void BubbleDialogDelegate::SetUseAnchorWindowBounds(bool use_anchor_bounds) {
use_anchor_window_bounds_ = use_anchor_bounds;
if (auto* frame_view = GetBubbleFrameView()) {
frame_view->set_use_anchor_window_bounds(use_anchor_bounds);
}
}
void BubbleDialogDelegate::SizeToContents() {
if (GetWidget() && !GetWidget()->IsClosed()) {
GetWidget()->SetBounds(GetDesiredWidgetBounds());
}
}
std::u16string BubbleDialogDelegate::GetSubtitle() const {
return subtitle_;
}
void BubbleDialogDelegate::SetSubtitle(const std::u16string& subtitle) {
if (subtitle_ == subtitle) {
return;
}
subtitle_ = subtitle;
BubbleFrameView* frame_view = GetBubbleFrameView();
if (frame_view) {
frame_view->UpdateSubtitle();
}
}
bool BubbleDialogDelegate::GetSubtitleAllowCharacterBreak() const {
return subtitle_allow_character_break_;
}
void BubbleDialogDelegate::SetSubtitleAllowCharacterBreak(bool allow) {
if (subtitle_allow_character_break_ == allow) {
return;
}
subtitle_allow_character_break_ = allow;
BubbleFrameView* frame_view = GetBubbleFrameView();
if (frame_view) {
frame_view->UpdateSubtitle();
}
}
void BubbleDialogDelegate::UpdateFrameColor() {
View* const contents_view = GetContentsView();
DCHECK(contents_view);
BubbleFrameView* frame_view = GetBubbleFrameView();
if (frame_view) {
frame_view->SetBackgroundColor(background_color());
}
// If layer_type() is LAYER_NOT_DRAWN, then the BubbleFrameView (and
// BubbleBorderBackground) will not be painted, so shouldn't worry about what
// contents are doing.
const bool contents_layer_opaque =
layer_type() != ui::LAYER_NOT_DRAWN && contents_view->layer() &&
!contents_view->layer()->AsNotDrawn() &&
contents_view->layer()->fills_bounds_opaquely();
if (contents_layer_opaque) {
CHECK(contents_view->background())
<< "If contents paint to an opaque layer, the bubble border background "
"won't show through, so explicitly paint a background color";
}
}
void BubbleDialogDelegate::OnBubbleWidgetVisibilityChanged(bool visible) {
// Log time from bubble dialog delegate creation to bubble becoming
// visible.
if (visible) {
if (GetWidget()->IsClosed()) {
return;
}
if (bubble_created_time_.has_value()) {
GetWidget()
->GetCompositor()
->RequestSuccessfulPresentationTimeForNextFrame(base::BindOnce(
[](base::WeakPtr<BubbleDialogDelegate::BubbleUmaLogger>
uma_logger,
base::TimeTicks bubble_created_time,
const viz::FrameTimingDetails& frame_timing_details) {
base::TimeTicks presentation_timestamp =
frame_timing_details.presentation_feedback.timestamp;
if (!uma_logger) {
return;
}
uma_logger->LogMetric(
base::UmaHistogramMediumTimes, "CreateToVisibleTime",
presentation_timestamp - bubble_created_time);
},
bubble_uma_logger().GetWeakPtr(), *bubble_created_time_));
bubble_created_time_.reset();
}
bubble_shown_time_ = base::TimeTicks::Now();
} else {
if (bubble_shown_time_.has_value()) {
bubble_shown_duration_ += base::TimeTicks::Now() - *bubble_shown_time_;
bubble_shown_time_.reset();
}
}
if (anchor_view_observer_) {
anchor_view_observer_->UpdateBubbleVisibility(visible);
}
UpdateHighlightedButton(visible);
// Fire ax::mojom::Event::kAlert for bubbles marked as
// ax::mojom::Role::kAlertDialog; this instructs accessibility tools to read
// the bubble in its entirety rather than just its title and initially focused
// view. See http://crbug.com/474622 for details.
if (visible && ui::IsAlert(GetAccessibleWindowRole())) {
GetWidget()->GetRootView()->GetViewAccessibility().SetRole(
GetAccessibleWindowRole());
GetWidget()->GetRootView()->NotifyAccessibilityEventDeprecated(
ax::mojom::Event::kAlert, true);
}
}
void BubbleDialogDelegate::OnDeactivate() {
if (ShouldCloseOnDeactivate() && GetWidget()) {
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kLostFocus);
}
}
void BubbleDialogDelegate::NotifyAnchoredBubbleIsPrimary() {
const bool visible = GetWidget() && GetWidget()->IsVisible();
UpdateHighlightedButton(visible);
SetAnchoredDialogKey();
}
void BubbleDialogDelegate::SetAnchoredDialogKey() {
auto* anchor_view = GetAnchorView();
DCHECK(anchor_view);
if (focus_traversable_from_anchor_view_) {
// Make sure that focus can move into here from the anchor view (but not
// out, focus will cycle inside the dialog once it gets here).
// It is possible that a view anchors more than one widgets,
// but among them there should be at most one widget that is focusable.
auto* old_anchored_dialog = anchor_view->GetProperty(kAnchoredDialogKey);
if (old_anchored_dialog && old_anchored_dialog != this) {
DLOG(WARNING) << "|anchor_view| has already anchored a focusable widget.";
}
anchor_view->SetProperty(kAnchoredDialogKey,
static_cast<DialogDelegate*>(this));
}
}
void BubbleDialogDelegate::UpdateHighlightedButton(bool highlighted) {
Button* button = nullptr;
ui::TrackedElement* element = highlighted_element_tracker_.get();
// Prioritize explicitly set highlights.
if (!element) {
button = Button::AsButton(GetAnchorView());
element = anchor_tracked_element_.get();
}
if (highlight_button_when_shown_) {
if (highlighted && (button || element)) {
if (button) {
button_anchor_highlight_ = button->AddAnchorHighlight();
element_anchor_highlight_.reset();
} else {
element_anchor_highlight_ =
ui::ElementHighlighter::GetElementHighlighter()->AddHighlight(
element);
button_anchor_highlight_.reset();
}
} else {
button_anchor_highlight_.reset();
element_anchor_highlight_.reset();
}
}
}
BEGIN_METADATA(BubbleDialogDelegateView)
END_METADATA
} // namespace views