blob: 059458a0e640905afbefea1919b82f3c47fa7a01 [file] [log] [blame]
// Copyright (c) 2012 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/tray_bubble_view.h"
#include <algorithm>
#include "base/macros.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/effects/SkBlurImageFilter.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_delegate.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/path.h"
#include "ui/gfx/skia_util.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/bubble/bubble_window_targeter.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/widget/widget.h"
namespace {
// Inset the arrow a bit from the edge.
const int kArrowMinOffset = 20;
const int kBubbleSpacing = 20;
// The new theme adjusts the menus / bubbles to be flush with the shelf when
// there is no bubble. These are the offsets which need to be applied.
const int kArrowOffsetTopBottom = 4;
const int kArrowOffsetLeft = 9;
const int kArrowOffsetRight = -5;
const int kOffsetLeftRightForTopBottomOrientation = 5;
// The sampling time for mouse position changes in ms - which is roughly a frame
// time.
const int kFrameTimeInMS = 30;
} // namespace
namespace views {
namespace internal {
// Detects any mouse movement. This is needed to detect mouse movements by the
// user over the bubble if the bubble got created underneath the cursor.
class MouseMoveDetectorHost : public MouseWatcherHost {
public:
MouseMoveDetectorHost();
~MouseMoveDetectorHost() override;
bool Contains(const gfx::Point& screen_point, MouseEventType type) override;
private:
DISALLOW_COPY_AND_ASSIGN(MouseMoveDetectorHost);
};
MouseMoveDetectorHost::MouseMoveDetectorHost() {
}
MouseMoveDetectorHost::~MouseMoveDetectorHost() {
}
bool MouseMoveDetectorHost::Contains(const gfx::Point& screen_point,
MouseEventType type) {
return false;
}
// Custom border for TrayBubbleView. Contains special logic for GetBounds()
// to stack bubbles with no arrows correctly. Also calculates the arrow offset.
class TrayBubbleBorder : public BubbleBorder {
public:
TrayBubbleBorder(View* owner,
View* anchor,
TrayBubbleView::InitParams params)
: BubbleBorder(params.arrow, params.shadow, params.arrow_color),
owner_(owner),
anchor_(anchor),
tray_arrow_offset_(params.arrow_offset),
first_item_has_no_margin_(params.first_item_has_no_margin) {
set_alignment(params.arrow_alignment);
set_background_color(params.arrow_color);
set_paint_arrow(params.arrow_paint_type);
}
~TrayBubbleBorder() override {}
// Overridden from BubbleBorder.
// Sets the bubble on top of the anchor when it has no arrow.
gfx::Rect GetBounds(const gfx::Rect& position_relative_to,
const gfx::Size& contents_size) const override {
if (has_arrow(arrow())) {
gfx::Rect rect =
BubbleBorder::GetBounds(position_relative_to, contents_size);
if (first_item_has_no_margin_) {
if (arrow() == BubbleBorder::BOTTOM_RIGHT ||
arrow() == BubbleBorder::BOTTOM_LEFT) {
rect.set_y(rect.y() + kArrowOffsetTopBottom);
int rtl_factor = base::i18n::IsRTL() ? -1 : 1;
rect.set_x(rect.x() +
rtl_factor * kOffsetLeftRightForTopBottomOrientation);
} else if (arrow() == BubbleBorder::LEFT_BOTTOM) {
rect.set_x(rect.x() + kArrowOffsetLeft);
} else if (arrow() == BubbleBorder::RIGHT_BOTTOM) {
rect.set_x(rect.x() + kArrowOffsetRight);
}
}
return rect;
}
gfx::Size border_size(contents_size);
gfx::Insets insets = GetInsets();
border_size.Enlarge(insets.width(), insets.height());
const int x = position_relative_to.x() +
position_relative_to.width() / 2 - border_size.width() / 2;
// Position the bubble on top of the anchor.
const int y = position_relative_to.y() - border_size.height() +
insets.height() - kBubbleSpacing;
return gfx::Rect(x, y, border_size.width(), border_size.height());
}
void UpdateArrowOffset() {
int arrow_offset = 0;
if (arrow() == BubbleBorder::BOTTOM_RIGHT ||
arrow() == BubbleBorder::BOTTOM_LEFT) {
// Note: tray_arrow_offset_ is relative to the anchor widget.
if (tray_arrow_offset_ ==
TrayBubbleView::InitParams::kArrowDefaultOffset) {
arrow_offset = kArrowMinOffset;
} else {
const int width = owner_->GetWidget()->GetContentsView()->width();
gfx::Point pt(tray_arrow_offset_, 0);
View::ConvertPointToScreen(anchor_->GetWidget()->GetRootView(), &pt);
View::ConvertPointFromScreen(owner_->GetWidget()->GetRootView(), &pt);
arrow_offset = pt.x();
if (arrow() == BubbleBorder::BOTTOM_RIGHT)
arrow_offset = width - arrow_offset;
arrow_offset = std::max(arrow_offset, kArrowMinOffset);
}
} else {
if (tray_arrow_offset_ ==
TrayBubbleView::InitParams::kArrowDefaultOffset) {
arrow_offset = kArrowMinOffset;
} else {
gfx::Point pt(0, tray_arrow_offset_);
View::ConvertPointToScreen(anchor_->GetWidget()->GetRootView(), &pt);
View::ConvertPointFromScreen(owner_->GetWidget()->GetRootView(), &pt);
arrow_offset = pt.y();
arrow_offset = std::max(arrow_offset, kArrowMinOffset);
}
}
set_arrow_offset(arrow_offset);
}
private:
View* owner_;
View* anchor_;
const int tray_arrow_offset_;
// If true the first item should not get any additional spacing against the
// anchor (without the bubble tip the bubble should be flush to the shelf).
const bool first_item_has_no_margin_;
DISALLOW_COPY_AND_ASSIGN(TrayBubbleBorder);
};
// This mask layer clips the bubble's content so that it does not overwrite the
// rounded bubble corners.
// TODO(miket): This does not work on Windows. Implement layer masking or
// alternate solutions if the TrayBubbleView is needed there in the future.
class TrayBubbleContentMask : public ui::LayerDelegate {
public:
explicit TrayBubbleContentMask(int corner_radius);
~TrayBubbleContentMask() override;
ui::Layer* layer() { return &layer_; }
// Overridden from LayerDelegate.
void OnPaintLayer(const ui::PaintContext& context) override;
void OnDelegatedFrameDamage(const gfx::Rect& damage_rect_in_dip) override {}
void OnDeviceScaleFactorChanged(float device_scale_factor) override;
base::Closure PrepareForLayerBoundsChange() override;
private:
ui::Layer layer_;
int corner_radius_;
DISALLOW_COPY_AND_ASSIGN(TrayBubbleContentMask);
};
TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius)
: layer_(ui::LAYER_TEXTURED),
corner_radius_(corner_radius) {
layer_.set_delegate(this);
layer_.SetFillsBoundsOpaquely(false);
}
TrayBubbleContentMask::~TrayBubbleContentMask() {
layer_.set_delegate(NULL);
}
void TrayBubbleContentMask::OnPaintLayer(const ui::PaintContext& context) {
ui::PaintRecorder recorder(context, layer()->size());
SkPaint paint;
paint.setAlpha(255);
paint.setStyle(SkPaint::kFill_Style);
gfx::Rect rect(layer()->bounds().size());
recorder.canvas()->DrawRoundRect(rect, corner_radius_, paint);
}
void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
float device_scale_factor) {
// Redrawing will take care of scale factor change.
}
base::Closure TrayBubbleContentMask::PrepareForLayerBoundsChange() {
return base::Closure();
}
// Custom layout for the bubble-view. Does the default box-layout if there is
// enough height. Otherwise, makes sure the bottom rows are visible.
class BottomAlignedBoxLayout : public BoxLayout {
public:
explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view)
: BoxLayout(BoxLayout::kVertical, 0, 0, 0),
bubble_view_(bubble_view) {
}
~BottomAlignedBoxLayout() override {}
private:
void Layout(View* host) override {
if (host->height() >= host->GetPreferredSize().height() ||
!bubble_view_->is_gesture_dragging()) {
BoxLayout::Layout(host);
return;
}
int consumed_height = 0;
for (int i = host->child_count() - 1;
i >= 0 && consumed_height < host->height(); --i) {
View* child = host->child_at(i);
if (!child->visible())
continue;
gfx::Size size = child->GetPreferredSize();
child->SetBounds(0, host->height() - consumed_height - size.height(),
host->width(), size.height());
consumed_height += size.height();
}
}
TrayBubbleView* bubble_view_;
DISALLOW_COPY_AND_ASSIGN(BottomAlignedBoxLayout);
};
} // namespace internal
using internal::TrayBubbleBorder;
using internal::TrayBubbleContentMask;
using internal::BottomAlignedBoxLayout;
// static
const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1;
TrayBubbleView::InitParams::InitParams(AnchorType anchor_type,
AnchorAlignment anchor_alignment,
int min_width,
int max_width)
: anchor_type(anchor_type),
anchor_alignment(anchor_alignment),
min_width(min_width),
max_width(max_width),
max_height(0),
can_activate(false),
close_on_deactivate(true),
arrow_color(SK_ColorBLACK),
first_item_has_no_margin(false),
arrow(BubbleBorder::NONE),
arrow_offset(kArrowDefaultOffset),
arrow_paint_type(BubbleBorder::PAINT_NORMAL),
shadow(BubbleBorder::BIG_SHADOW),
arrow_alignment(BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE) {
}
TrayBubbleView::InitParams::InitParams(const InitParams& other) = default;
// static
TrayBubbleView* TrayBubbleView::Create(View* anchor,
Delegate* delegate,
InitParams* init_params) {
DCHECK(anchor);
// Set arrow here so that it can be passed to the BubbleView constructor.
if (init_params->anchor_type == ANCHOR_TYPE_TRAY) {
if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_BOTTOM) {
init_params->arrow = base::i18n::IsRTL() ?
BubbleBorder::BOTTOM_LEFT : BubbleBorder::BOTTOM_RIGHT;
} else if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_TOP) {
init_params->arrow = BubbleBorder::TOP_LEFT;
} else if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_LEFT) {
init_params->arrow = BubbleBorder::LEFT_BOTTOM;
} else {
init_params->arrow = BubbleBorder::RIGHT_BOTTOM;
}
} else {
init_params->arrow = BubbleBorder::NONE;
}
return new TrayBubbleView(anchor, delegate, *init_params);
}
TrayBubbleView::TrayBubbleView(View* anchor,
Delegate* delegate,
const InitParams& init_params)
: BubbleDialogDelegateView(anchor, init_params.arrow),
params_(init_params),
delegate_(delegate),
preferred_width_(init_params.min_width),
bubble_border_(new TrayBubbleBorder(this, GetAnchorView(), init_params)),
owned_bubble_border_(bubble_border_),
is_gesture_dragging_(false),
mouse_actively_entered_(false) {
set_can_activate(params_.can_activate);
DCHECK(anchor_widget()); // Computed by BubbleDialogDelegateView().
set_notify_enter_exit_on_child(true);
set_close_on_deactivate(init_params.close_on_deactivate);
set_margins(gfx::Insets());
SetPaintToLayer(true);
bubble_content_mask_.reset(
new TrayBubbleContentMask(bubble_border_->GetBorderCornerRadius()));
}
TrayBubbleView::~TrayBubbleView() {
mouse_watcher_.reset();
// Inform host items (models) that their views are being destroyed.
if (delegate_)
delegate_->BubbleViewDestroyed();
}
void TrayBubbleView::InitializeAndShowBubble() {
// Must occur after call to BubbleDialogDelegateView::CreateBubble().
SetAlignment(params_.arrow_alignment);
bubble_border_->UpdateArrowOffset();
layer()->parent()->SetMaskLayer(bubble_content_mask_->layer());
GetWidget()->Show();
GetWidget()->GetNativeWindow()->SetEventTargeter(
std::unique_ptr<ui::EventTargeter>(new BubbleWindowTargeter(this)));
UpdateBubble();
}
void TrayBubbleView::UpdateBubble() {
if (GetWidget()) {
SizeToContents();
bubble_content_mask_->layer()->SetBounds(layer()->bounds());
GetWidget()->GetRootView()->SchedulePaint();
}
}
void TrayBubbleView::SetMaxHeight(int height) {
params_.max_height = height;
if (GetWidget())
SizeToContents();
}
void TrayBubbleView::SetWidth(int width) {
width = std::max(std::min(width, params_.max_width), params_.min_width);
if (preferred_width_ == width)
return;
preferred_width_ = width;
if (GetWidget())
SizeToContents();
}
void TrayBubbleView::SetArrowPaintType(
views::BubbleBorder::ArrowPaintType paint_type) {
bubble_border_->set_paint_arrow(paint_type);
UpdateBubble();
}
gfx::Insets TrayBubbleView::GetBorderInsets() const {
return bubble_border_->GetInsets();
}
int TrayBubbleView::GetDialogButtons() const {
return ui::DIALOG_BUTTON_NONE;
}
void TrayBubbleView::Init() {
BoxLayout* layout = new BottomAlignedBoxLayout(this);
layout->SetDefaultFlex(1);
SetLayoutManager(layout);
}
gfx::Rect TrayBubbleView::GetAnchorRect() const {
if (!delegate_)
return gfx::Rect();
return delegate_->GetAnchorRect(anchor_widget(),
params_.anchor_type,
params_.anchor_alignment);
}
void TrayBubbleView::OnBeforeBubbleWidgetInit(Widget::InitParams* params,
Widget* bubble_widget) const {
if (delegate_)
delegate_->OnBeforeBubbleWidgetInit(anchor_widget(), bubble_widget, params);
}
NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
BubbleFrameView* frame = static_cast<BubbleFrameView*>(
BubbleDialogDelegateView::CreateNonClientFrameView(widget));
frame->SetBubbleBorder(std::move(owned_bubble_border_));
return frame;
}
bool TrayBubbleView::WidgetHasHitTestMask() const {
return true;
}
void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const {
DCHECK(mask);
mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
}
gfx::Size TrayBubbleView::GetPreferredSize() const {
return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
}
gfx::Size TrayBubbleView::GetMaximumSize() const {
gfx::Size size = GetPreferredSize();
size.set_width(params_.max_width);
return size;
}
int TrayBubbleView::GetHeightForWidth(int width) const {
int height = GetInsets().height();
width = std::max(width - GetInsets().width(), 0);
for (int i = 0; i < child_count(); ++i) {
const View* child = child_at(i);
if (child->visible())
height += child->GetHeightForWidth(width);
}
return (params_.max_height != 0) ?
std::min(height, params_.max_height) : height;
}
void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
mouse_watcher_.reset();
if (delegate_ && !(event.flags() & ui::EF_IS_SYNTHESIZED)) {
// Coming here the user was actively moving the mouse over the bubble and
// we inform the delegate that we entered. This will prevent the bubble
// to auto close.
delegate_->OnMouseEnteredView();
mouse_actively_entered_ = true;
} else {
// Coming here the bubble got shown and the mouse was 'accidentally' over it
// which is not a reason to prevent the bubble to auto close. As such we
// do not call the delegate, but wait for the first mouse move within the
// bubble. The used MouseWatcher will notify use of a movement and call
// |MouseMovedOutOfHost|.
mouse_watcher_.reset(new MouseWatcher(
new views::internal::MouseMoveDetectorHost(),
this));
// Set the mouse sampling frequency to roughly a frame time so that the user
// cannot see a lag.
mouse_watcher_->set_notify_on_exit_time(
base::TimeDelta::FromMilliseconds(kFrameTimeInMS));
mouse_watcher_->Start();
}
}
void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) {
// If there was a mouse watcher waiting for mouse movements we disable it
// immediately since we now leave the bubble.
mouse_watcher_.reset();
// Do not notify the delegate of an exit if we never told it that we entered.
if (delegate_ && mouse_actively_entered_)
delegate_->OnMouseExitedView();
}
void TrayBubbleView::GetAccessibleState(ui::AXViewState* state) {
if (delegate_ && CanActivate()) {
state->role = ui::AX_ROLE_WINDOW;
state->name = delegate_->GetAccessibleNameForBubble();
}
}
void TrayBubbleView::MouseMovedOutOfHost() {
// The mouse was accidentally over the bubble when it opened and the AutoClose
// logic was not activated. Now that the user did move the mouse we tell the
// delegate to disable AutoClose.
delegate_->OnMouseEnteredView();
mouse_actively_entered_ = true;
mouse_watcher_->Stop();
}
void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
SizeToContents();
}
void TrayBubbleView::ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) {
if (details.is_add && details.child == this) {
details.parent->SetPaintToLayer(true);
details.parent->layer()->SetMasksToBounds(true);
}
}
} // namespace views