blob: 7c4fb6a5a4d48ca8ccf17d2980b416a7732a3b5d [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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 <utility>
#include "base/bind.h"
#include "base/metrics/histogram_macros.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/default_style.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/compositor/layer_animation_element.h"
#include "ui/display/screen.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/vector2d_conversions.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/layout/layout_manager.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/metadata/metadata_impl_macros.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#if defined(OS_WIN)
#include "ui/base/win/shell.h"
#endif
#if defined(OS_APPLE)
#include "ui/views/widget/widget_utils_mac.h"
#else
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#endif
namespace views {
// static
bool BubbleDialogDelegate::devtools_dismiss_override_ = false;
namespace {
// A BubbleFrameView will apply a masking path to its ClientView to ensure
// contents are appropriately clipped to the frame's rounded corners. If the
// bubble uses layers in its views hierarchy, these will not be clipped to
// the client mask unless the ClientView is backed by a textured ui::Layer.
// This flag tracks whether or not to to create a layer backed ClientView.
//
// TODO(tluk): Fix all cases where bubble transparency is used and have bubble
// ClientViews always paint to a layer.
// TODO(tluk): Flip this to true for all bubbles.
DEFINE_UI_CLASS_PROPERTY_KEY(bool, kPaintClientToLayer, false)
// 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;
// Widget:
const ui::ThemeProvider* GetThemeProvider() const override {
BubbleDialogDelegateView* const bubble_delegate =
static_cast<BubbleDialogDelegateView*>(widget_delegate());
if (!bubble_delegate || !bubble_delegate->anchor_widget())
return Widget::GetThemeProvider();
return bubble_delegate->anchor_widget()->GetThemeProvider();
}
private:
DISALLOW_COPY_AND_ASSIGN(BubbleWidget);
};
// 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()) {}
// View:
gfx::Size GetMinimumSize() const override { return gfx::Size(); }
gfx::Size GetMaximumSize() const override { return gfx::Size(); }
private:
DISALLOW_COPY_AND_ASSIGN(BubbleDialogFrameView);
};
bool CustomShadowsSupported() {
#if defined(OS_WIN)
return ui::win::IsAeroGlassEnabled();
#else
return true;
#endif
}
// Create a widget to host the bubble.
Widget* CreateBubbleWidget(BubbleDialogDelegate* bubble) {
Widget* bubble_widget = new BubbleWidget();
Widget::InitParams bubble_params(Widget::InitParams::TYPE_BUBBLE);
bubble_params.delegate = bubble;
bubble_params.opacity = CustomShadowsSupported()
? Widget::InitParams::WindowOpacity::kTranslucent
: Widget::InitParams::WindowOpacity::kOpaque;
bubble_params.accept_events = bubble->accept_events();
bubble_params.remove_standard_frame = true;
bubble_params.layer_type = bubble->GetLayerType();
// Use a window default shadow if the bubble doesn't provides its own.
if (bubble->GetShadow() == BubbleBorder::NO_ASSETS)
bubble_params.shadow_type = Widget::InitParams::ShadowType::kDefault;
else if (CustomShadowsSupported())
bubble_params.shadow_type = Widget::InitParams::ShadowType::kNone;
else
bubble_params.shadow_type = Widget::InitParams::ShadowType::kDrop;
if (bubble->parent_window()) {
bubble_params.parent = bubble->parent_window();
} else if (bubble->anchor_widget()) {
bubble_params.parent = bubble->anchor_widget()->GetNativeView();
}
bubble_params.activatable = bubble->CanActivate()
? Widget::InitParams::ACTIVATABLE_YES
: Widget::InitParams::ACTIVATABLE_NO;
bubble->OnBeforeBubbleWidgetInit(&bubble_params, bubble_widget);
DCHECK(bubble_params.parent);
bubble_widget->Init(std::move(bubble_params));
#if !defined(OS_APPLE)
// On Mac, having a parent window creates a permanent stacking order, so
// there's no need to do this. Also, calling StackAbove() on Mac shows the
// bubble implicitly, for which the bubble is currently not ready.
if (bubble_params.parent)
bubble_widget->StackAbove(bubble_params.parent);
#endif
return bubble_widget;
}
} // namespace
class BubbleDialogDelegate::AnchorViewObserver : public ViewObserver {
public:
AnchorViewObserver(BubbleDialogDelegate* parent, View* anchor_view)
: parent_(parent), anchor_view_(anchor_view) {
anchor_view_->AddObserver(this);
}
AnchorViewObserver(const AnchorViewObserver&) = delete;
AnchorViewObserver& operator=(const AnchorViewObserver&) = delete;
~AnchorViewObserver() override { anchor_view_->RemoveObserver(this); }
View* anchor_view() const { return anchor_view_; }
// 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);
parent_->SetAnchorView(nullptr);
}
void OnViewBoundsChanged(View* observed_view) override {
// This code really wants to know the anchor bounds in screen coordinates
// have changed. There isn't a good way to detect this outside of the view.
// Observing View bounds changing catches some cases but not all of them.
DCHECK_EQ(anchor_view_, observed_view);
parent_->OnAnchorBoundsChanged();
}
// TODO(pbos): Consider observing View visibility changes and only updating
// view bounds when the anchor is visible.
private:
BubbleDialogDelegate* const parent_;
View* const anchor_view_;
};
// This class is responsible for observing events on a BubbleDialogDelegate's
// anchor widget and notifying the BubbleDialogDelegate of them.
#if defined(OS_APPLE)
class BubbleDialogDelegate::AnchorWidgetObserver : public WidgetObserver {
#else
class BubbleDialogDelegate::AnchorWidgetObserver : public WidgetObserver,
public aura::WindowObserver {
#endif
public:
AnchorWidgetObserver(BubbleDialogDelegate* owner, Widget* widget)
: owner_(owner) {
widget_observer_.Add(widget);
#if !defined(OS_APPLE)
window_observer_.Add(widget->GetNativeWindow());
#endif
}
~AnchorWidgetObserver() override = default;
// WidgetObserver:
void OnWidgetDestroying(Widget* widget) override {
#if !defined(OS_APPLE)
window_observer_.Remove(widget->GetNativeWindow());
#endif
widget_observer_.Remove(widget);
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();
}
#if !defined(OS_APPLE)
// 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();
}
}
#endif
private:
BubbleDialogDelegate* owner_;
ScopedObserver<views::Widget, views::WidgetObserver> widget_observer_{this};
#if !defined(OS_APPLE)
ScopedObserver<aura::Window, aura::WindowObserver> window_observer_{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) {
observer_.Add(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 {
observer_.Remove(widget);
owner_->OnWidgetDestroyed(widget);
}
void OnWidgetBoundsChanged(Widget* widget, const gfx::Rect& bounds) override {
owner_->OnWidgetBoundsChanged(widget, bounds);
}
void OnWidgetVisibilityChanging(Widget* widget, bool visible) override {
#if defined(OS_WIN)
// On Windows we need to handle this before the bubble is visible or hidden.
// Please see the comment on the OnWidgetVisibilityChanging function. On
// other platforms it is fine to handle it after the bubble is shown/hidden.
owner_->OnBubbleWidgetVisibilityChanged(visible);
#endif
}
void OnWidgetVisibilityChanged(Widget* widget, bool visible) override {
#if !defined(OS_WIN)
owner_->OnBubbleWidgetVisibilityChanged(visible);
#endif
owner_->OnWidgetVisibilityChanged(widget, visible);
}
void OnWidgetActivationChanged(Widget* widget, bool active) override {
owner_->OnBubbleWidgetActivationChanged(active);
owner_->OnWidgetActivationChanged(widget, active);
}
private:
BubbleDialogDelegate* owner_;
ScopedObserver<views::Widget, views::WidgetObserver> observer_{this};
};
BubbleDialogDelegate::BubbleDialogDelegate() = default;
BubbleDialogDelegate::BubbleDialogDelegate(View* anchor_view,
BubbleBorder::Arrow arrow,
BubbleBorder::Shadow shadow)
: arrow_(arrow), shadow_(shadow) {}
BubbleDialogDelegate::~BubbleDialogDelegate() = default;
// static
Widget* BubbleDialogDelegate::CreateBubble(
BubbleDialogDelegate* bubble_delegate) {
// On Mac, MODAL_TYPE_WINDOW 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 (bubble_delegate->GetModalType() == ui::MODAL_TYPE_WINDOW) {
DCHECK(!bubble_delegate->GetAnchorView());
DCHECK_EQ(bubble_delegate->GetAnchorRect(), gfx::Rect());
}
bubble_delegate->Init();
// Get the latest anchor widget from the anchor view at bubble creation time.
bubble_delegate->SetAnchorView(bubble_delegate->GetAnchorView());
Widget* bubble_widget = CreateBubbleWidget(bubble_delegate);
bubble_delegate->set_adjust_if_offscreen(
PlatformStyle::kAdjustBubbleIfOffscreen);
bubble_delegate->SizeToContents();
bubble_delegate->bubble_widget_observer_ =
std::make_unique<BubbleWidgetObserver>(bubble_delegate, bubble_widget);
bubble_delegate->paint_as_active_subscription_ =
bubble_widget->RegisterPaintAsActiveChangedCallback(base::BindRepeating(
&BubbleDialogDelegate::OnBubbleWidgetPaintAsActiveChanged,
base::Unretained(bubble_delegate)));
return bubble_widget;
}
Widget* BubbleDialogDelegateView::CreateBubble(
std::unique_ptr<BubbleDialogDelegateView> delegate) {
return CreateBubble(delegate.release());
}
Widget* BubbleDialogDelegateView::CreateBubble(BubbleDialogDelegateView* view) {
return BubbleDialogDelegate::CreateBubble(view);
}
BubbleDialogDelegateView::BubbleDialogDelegateView()
: BubbleDialogDelegateView(nullptr, BubbleBorder::TOP_LEFT) {}
BubbleDialogDelegateView::BubbleDialogDelegateView(View* anchor_view,
BubbleBorder::Arrow arrow,
BubbleBorder::Shadow shadow)
: BubbleDialogDelegate(anchor_view, arrow, shadow) {
set_owned_by_client();
SetOwnedByWidget(true);
WidgetDelegate::SetShowCloseButton(false);
SetArrow(arrow);
LayoutProvider* provider = LayoutProvider::Get();
// An individual bubble should override these margins if its layout differs
// from the typical title/text/buttons.
set_margins(provider->GetDialogInsetsForContentType(TEXT, TEXT));
set_title_margins(provider->GetInsetsMetric(INSETS_DIALOG_TITLE));
if (anchor_view)
SetAnchorView(anchor_view);
UpdateColorsFromTheme();
UMA_HISTOGRAM_BOOLEAN("Dialog.BubbleDialogDelegateView.Create", true);
}
BubbleDialogDelegateView::~BubbleDialogDelegateView() {
SetLayoutManager(nullptr);
SetAnchorView(nullptr);
}
BubbleDialogDelegate* BubbleDialogDelegate::AsBubbleDialogDelegate() {
return this;
}
std::unique_ptr<NonClientFrameView>
BubbleDialogDelegate::CreateNonClientFrameView(Widget* widget) {
auto frame = std::make_unique<BubbleDialogFrameView>(title_margins_);
LayoutProvider* provider = LayoutProvider::Get();
frame->set_footnote_margins(
provider->GetInsetsMetric(INSETS_DIALOG_SUBSECTION));
frame->SetFootnoteView(DisownFootnoteView());
std::unique_ptr<BubbleBorder> border =
std::make_unique<BubbleBorder>(arrow(), GetShadow(), color());
if (CustomShadowsSupported() && GetParams().round_corners)
border->SetCornerRadius(GetCornerRadius());
frame->SetBubbleBorder(std::move(border));
return frame;
}
ClientView* BubbleDialogDelegate::CreateClientView(Widget* widget) {
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.
if (GetProperty(kPaintClientToLayer)) {
client_view_->SetPaintToLayer();
client_view_->layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF(GetCornerRadius()));
client_view_->layer()->SetIsFastRoundedCorner(true);
}
return client_view_;
}
bool BubbleDialogDelegateView::AcceleratorPressed(
const ui::Accelerator& accelerator) {
if (accelerator.key_code() == ui::VKEY_DOWN ||
accelerator.key_code() == ui::VKEY_UP) {
// Move the focus up or down.
GetFocusManager()->AdvanceFocus(accelerator.key_code() != ui::VKEY_DOWN);
return true;
}
return View::AcceleratorPressed(accelerator);
}
Widget* BubbleDialogDelegateView::GetWidget() {
return View::GetWidget();
}
const Widget* BubbleDialogDelegateView::GetWidget() const {
return View::GetWidget();
}
void BubbleDialogDelegateView::AddedToWidget() {
if (ui::IsAlert(GetAccessibleWindowRole())) {
GetWidget()->GetRootView()->NotifyAccessibilityEvent(
ax::mojom::Event::kAlert, true);
}
}
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);
}
void BubbleDialogDelegate::OnAnchorWidgetDestroying() {
SetAnchorView(nullptr);
}
void BubbleDialogDelegate::OnBubbleWidgetActivationChanged(bool active) {
if (devtools_dismiss_override_)
return;
#if defined(OS_APPLE)
// 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().GetNativeNSWindow(),
base::BindRepeating(&BubbleDialogDelegate::OnDeactivate,
base::Unretained(this)));
}
#endif
if (!active)
OnDeactivate();
}
void BubbleDialogDelegate::OnAnchorWidgetBoundsChanged() {
if (GetBubbleFrameView())
SizeToContents();
}
void BubbleDialogDelegate::OnBubbleWidgetPaintAsActiveChanged() {
// It's possible for GetWidget() to return null here when the Widget's
// ownership model is WIDGET_OWNS_NATIVE_WIDGET. In that case, the View
// hierarchy is torn down, which detaches rather than destroys |this| due to
// set_owned_by_client(). Then the native widget is destroyed, which calls
// back here. Since GetWidget() is implemented in terms of View::GetWidget(),
// which no longer has a RootView, it returns null. While there are other
// ways to address this, they all seem more fragile than null-checking.
if (!GetWidget() || !GetWidget()->ShouldPaintAsActive()) {
paint_as_active_lock_.reset();
return;
}
if (!anchor_widget() || !anchor_widget()->GetTopLevelWidget())
return;
// When this bubble renders as active, its anchor widget should also render as
// active.
paint_as_active_lock_ =
anchor_widget()->GetTopLevelWidget()->LockPaintAsActive();
}
BubbleBorder::Shadow BubbleDialogDelegate::GetShadow() const {
if (CustomShadowsSupported() || shadow_ == BubbleBorder::NO_ASSETS)
return shadow_;
return BubbleBorder::NO_SHADOW;
}
View* BubbleDialogDelegate::GetAnchorView() const {
if (!anchor_view_observer_)
return nullptr;
return anchor_view_observer_->anchor_view();
}
void BubbleDialogDelegate::SetHighlightedButton(Button* highlighted_button) {
bool visible = GetWidget() && GetWidget()->IsVisible();
// If the Widget is visible, ensure the old highlight (if any) is removed
// when the highlighted view changes.
if (visible && highlighted_button != highlighted_button_tracker_.view())
UpdateHighlightedButton(false);
highlighted_button_tracker_.SetView(highlighted_button);
if (visible)
UpdateHighlightedButton(true);
}
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.
if (!GetAnchorView())
return anchor_rect_.value_or(gfx::Rect());
anchor_rect_ = GetAnchorView()->GetAnchorBoundsInScreen();
anchor_rect_->Inset(anchor_view_insets_);
#if !defined(OS_APPLE)
// 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.
// TODO(sammiequon): Investigate if we can remove |anchor_widget_| and just
// replace its calls with GetAnchorView()->GetWidget().
DCHECK_EQ(anchor_widget_, GetAnchorView()->GetWidget());
gfx::Transform transform =
anchor_widget_->GetNativeWindow()->layer()->GetTargetTransform();
if (!transform.IsIdentity())
anchor_rect_->Offset(-gfx::ToRoundedVector2d(transform.To2dTranslation()));
#endif
return anchor_rect_.value();
}
ui::LayerType BubbleDialogDelegate::GetLayerType() const {
return ui::LAYER_TEXTURED;
}
void BubbleDialogDelegate::SetPaintClientToLayer(bool paint_client_to_layer) {
DCHECK(!client_view_);
SetProperty(kPaintClientToLayer, paint_client_to_layer);
}
void BubbleDialogDelegate::UseCompactMargins() {
set_margins(gfx::Insets(6));
}
// static
gfx::Size BubbleDialogDelegate::GetMaxAvailableScreenSpaceToPlaceBubble(
View* anchor_view,
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);
gfx::Rect anchor_rect = anchor_view->GetAnchorBoundsInScreen();
gfx::Rect screen_rect =
display::Screen::GetScreen()
->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())
return;
// TODO(pbos): Reconsider whether to update the anchor when the view isn't
// drawn.
SizeToContents();
}
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();
return GetBubbleFrameView()->GetUpdatedWindowBounds(
anchor_rect, arrow(), GetWidget()->client_view()->GetPreferredSize(),
adjust_if_offscreen_ && !anchor_minimized && has_anchor);
}
ax::mojom::Role BubbleDialogDelegate::GetAccessibleWindowRole() {
// 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::Size BubbleDialogDelegateView::GetMinimumSize() const {
// Note that although BubbleDialogFrameView will never invoke this, a subclass
// may override CreateNonClientFrameView() to provide a NonClientFrameView
// that does. See http://crbug.com/844359.
return gfx::Size();
}
gfx::Size BubbleDialogDelegateView::GetMaximumSize() const {
return gfx::Size();
}
void BubbleDialogDelegateView::OnThemeChanged() {
View::OnThemeChanged();
UpdateColorsFromTheme();
}
void BubbleDialogDelegateView::Init() {}
void BubbleDialogDelegate::SetAnchorView(View* anchor_view) {
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()) {
if (GetWidget() && GetWidget()->IsVisible())
UpdateHighlightedButton(false);
paint_as_active_lock_.reset();
anchor_widget_ = nullptr;
}
if (anchor_view) {
anchor_widget_ = anchor_view->GetWidget();
if (anchor_widget_) {
const bool visible = GetWidget() && GetWidget()->IsVisible();
UpdateHighlightedButton(visible);
// Have the anchor widget's paint-as-active state track this view's
// widget - lock is only required if the bubble widget is active.
if (anchor_widget_->GetTopLevelWidget() && GetWidget() &&
GetWidget()->ShouldPaintAsActive()) {
paint_as_active_lock_ =
anchor_widget_->GetTopLevelWidget()->LockPaintAsActive();
}
}
}
}
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();
}
if (anchor_view && 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::SetAnchorRect(const gfx::Rect& rect) {
anchor_rect_ = rect;
if (GetWidget())
OnAnchorBoundsChanged();
}
void BubbleDialogDelegate::SizeToContents() {
gfx::Rect bubble_bounds = GetBubbleBounds();
#if defined(OS_APPLE)
// 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
GetWidget()->SetBounds(bubble_bounds);
}
void BubbleDialogDelegateView::UpdateColorsFromTheme() {
if (!color_explicitly_set()) {
set_color_internal(GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_BubbleBackground));
}
BubbleFrameView* frame_view = GetBubbleFrameView();
if (frame_view)
frame_view->SetBackgroundColor(color());
// When there's an opaque layer, the bubble border background won't show
// through, so explicitly paint a background color.
SetBackground(layer() && layer()->fills_bounds_opaquely()
? CreateSolidBackground(color())
: nullptr);
}
void BubbleDialogDelegate::OnBubbleWidgetVisibilityChanged(bool 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) {
if (ui::IsAlert(GetAccessibleWindowRole())) {
GetWidget()->GetRootView()->NotifyAccessibilityEvent(
ax::mojom::Event::kAlert, true);
}
}
}
void BubbleDialogDelegate::OnDeactivate() {
if (close_on_deactivate_ && GetWidget())
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kLostFocus);
}
void BubbleDialogDelegate::UpdateHighlightedButton(bool highlighted) {
Button* button = Button::AsButton(highlighted_button_tracker_.view());
button = button ? button : Button::AsButton(GetAnchorView());
if (button && highlight_button_when_shown_)
button->SetHighlighted(highlighted);
}
BEGIN_METADATA(BubbleDialogDelegateView, View)
END_METADATA
} // namespace views