1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/views/bubble/tray_bubble_view.h"
9 #include "third_party/skia/include/core/SkCanvas.h"
10 #include "third_party/skia/include/core/SkColor.h"
11 #include "third_party/skia/include/core/SkPaint.h"
12 #include "third_party/skia/include/core/SkPath.h"
13 #include "third_party/skia/include/effects/SkBlurImageFilter.h"
14 #include "ui/accessibility/ax_view_state.h"
15 #include "ui/aura/window.h"
16 #include "ui/compositor/layer.h"
17 #include "ui/compositor/layer_delegate.h"
18 #include "ui/events/event.h"
19 #include "ui/gfx/canvas.h"
20 #include "ui/gfx/insets.h"
21 #include "ui/gfx/path.h"
22 #include "ui/gfx/rect.h"
23 #include "ui/gfx/skia_util.h"
24 #include "ui/views/bubble/bubble_frame_view.h"
25 #include "ui/views/bubble/bubble_window_targeter.h"
26 #include "ui/views/layout/box_layout.h"
27 #include "ui/views/widget/widget.h"
31 // Inset the arrow a bit from the edge.
32 const int kArrowMinOffset
= 20;
33 const int kBubbleSpacing
= 20;
35 // The new theme adjusts the menus / bubbles to be flush with the shelf when
36 // there is no bubble. These are the offsets which need to be applied.
37 const int kArrowOffsetTopBottom
= 4;
38 const int kArrowOffsetLeft
= 9;
39 const int kArrowOffsetRight
= -5;
40 const int kOffsetLeftRightForTopBottomOrientation
= 5;
42 // The sampling time for mouse position changes in ms - which is roughly a frame
44 const int kFrameTimeInMS
= 30;
51 // Detects any mouse movement. This is needed to detect mouse movements by the
52 // user over the bubble if the bubble got created underneath the cursor.
53 class MouseMoveDetectorHost
: public MouseWatcherHost
{
55 MouseMoveDetectorHost();
56 virtual ~MouseMoveDetectorHost();
58 virtual bool Contains(const gfx::Point
& screen_point
,
59 MouseEventType type
) OVERRIDE
;
61 DISALLOW_COPY_AND_ASSIGN(MouseMoveDetectorHost
);
64 MouseMoveDetectorHost::MouseMoveDetectorHost() {
67 MouseMoveDetectorHost::~MouseMoveDetectorHost() {
70 bool MouseMoveDetectorHost::Contains(const gfx::Point
& screen_point
,
71 MouseEventType type
) {
75 // Custom border for TrayBubbleView. Contains special logic for GetBounds()
76 // to stack bubbles with no arrows correctly. Also calculates the arrow offset.
77 class TrayBubbleBorder
: public BubbleBorder
{
79 TrayBubbleBorder(View
* owner
,
81 TrayBubbleView::InitParams params
)
82 : BubbleBorder(params
.arrow
, params
.shadow
, params
.arrow_color
),
85 tray_arrow_offset_(params
.arrow_offset
),
86 first_item_has_no_margin_(params
.first_item_has_no_margin
) {
87 set_alignment(params
.arrow_alignment
);
88 set_background_color(params
.arrow_color
);
89 set_paint_arrow(params
.arrow_paint_type
);
92 virtual ~TrayBubbleBorder() {}
94 // Overridden from BubbleBorder.
95 // Sets the bubble on top of the anchor when it has no arrow.
96 virtual gfx::Rect
GetBounds(const gfx::Rect
& position_relative_to
,
97 const gfx::Size
& contents_size
) const OVERRIDE
{
98 if (has_arrow(arrow())) {
100 BubbleBorder::GetBounds(position_relative_to
, contents_size
);
101 if (first_item_has_no_margin_
) {
102 if (arrow() == BubbleBorder::BOTTOM_RIGHT
||
103 arrow() == BubbleBorder::BOTTOM_LEFT
) {
104 rect
.set_y(rect
.y() + kArrowOffsetTopBottom
);
105 int rtl_factor
= base::i18n::IsRTL() ? -1 : 1;
106 rect
.set_x(rect
.x() +
107 rtl_factor
* kOffsetLeftRightForTopBottomOrientation
);
108 } else if (arrow() == BubbleBorder::LEFT_BOTTOM
) {
109 rect
.set_x(rect
.x() + kArrowOffsetLeft
);
110 } else if (arrow() == BubbleBorder::RIGHT_BOTTOM
) {
111 rect
.set_x(rect
.x() + kArrowOffsetRight
);
117 gfx::Size
border_size(contents_size
);
118 gfx::Insets insets
= GetInsets();
119 border_size
.Enlarge(insets
.width(), insets
.height());
120 const int x
= position_relative_to
.x() +
121 position_relative_to
.width() / 2 - border_size
.width() / 2;
122 // Position the bubble on top of the anchor.
123 const int y
= position_relative_to
.y() - border_size
.height() +
124 insets
.height() - kBubbleSpacing
;
125 return gfx::Rect(x
, y
, border_size
.width(), border_size
.height());
128 void UpdateArrowOffset() {
129 int arrow_offset
= 0;
130 if (arrow() == BubbleBorder::BOTTOM_RIGHT
||
131 arrow() == BubbleBorder::BOTTOM_LEFT
) {
132 // Note: tray_arrow_offset_ is relative to the anchor widget.
133 if (tray_arrow_offset_
==
134 TrayBubbleView::InitParams::kArrowDefaultOffset
) {
135 arrow_offset
= kArrowMinOffset
;
137 const int width
= owner_
->GetWidget()->GetContentsView()->width();
138 gfx::Point
pt(tray_arrow_offset_
, 0);
139 View::ConvertPointToScreen(anchor_
->GetWidget()->GetRootView(), &pt
);
140 View::ConvertPointFromScreen(owner_
->GetWidget()->GetRootView(), &pt
);
141 arrow_offset
= pt
.x();
142 if (arrow() == BubbleBorder::BOTTOM_RIGHT
)
143 arrow_offset
= width
- arrow_offset
;
144 arrow_offset
= std::max(arrow_offset
, kArrowMinOffset
);
147 if (tray_arrow_offset_
==
148 TrayBubbleView::InitParams::kArrowDefaultOffset
) {
149 arrow_offset
= kArrowMinOffset
;
151 gfx::Point
pt(0, tray_arrow_offset_
);
152 View::ConvertPointToScreen(anchor_
->GetWidget()->GetRootView(), &pt
);
153 View::ConvertPointFromScreen(owner_
->GetWidget()->GetRootView(), &pt
);
154 arrow_offset
= pt
.y();
155 arrow_offset
= std::max(arrow_offset
, kArrowMinOffset
);
158 set_arrow_offset(arrow_offset
);
164 const int tray_arrow_offset_
;
166 // If true the first item should not get any additional spacing against the
167 // anchor (without the bubble tip the bubble should be flush to the shelf).
168 const bool first_item_has_no_margin_
;
170 DISALLOW_COPY_AND_ASSIGN(TrayBubbleBorder
);
173 // This mask layer clips the bubble's content so that it does not overwrite the
174 // rounded bubble corners.
175 // TODO(miket): This does not work on Windows. Implement layer masking or
176 // alternate solutions if the TrayBubbleView is needed there in the future.
177 class TrayBubbleContentMask
: public ui::LayerDelegate
{
179 explicit TrayBubbleContentMask(int corner_radius
);
180 virtual ~TrayBubbleContentMask();
182 ui::Layer
* layer() { return &layer_
; }
184 // Overridden from LayerDelegate.
185 virtual void OnPaintLayer(gfx::Canvas
* canvas
) OVERRIDE
;
186 virtual void OnDelegatedFrameDamage(
187 const gfx::Rect
& damage_rect_in_dip
) OVERRIDE
{}
188 virtual void OnDeviceScaleFactorChanged(float device_scale_factor
) OVERRIDE
;
189 virtual base::Closure
PrepareForLayerBoundsChange() OVERRIDE
;
193 SkScalar corner_radius_
;
195 DISALLOW_COPY_AND_ASSIGN(TrayBubbleContentMask
);
198 TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius
)
199 : layer_(ui::LAYER_TEXTURED
),
200 corner_radius_(corner_radius
) {
201 layer_
.set_delegate(this);
204 TrayBubbleContentMask::~TrayBubbleContentMask() {
205 layer_
.set_delegate(NULL
);
208 void TrayBubbleContentMask::OnPaintLayer(gfx::Canvas
* canvas
) {
210 path
.addRoundRect(gfx::RectToSkRect(gfx::Rect(layer()->bounds().size())),
211 corner_radius_
, corner_radius_
);
214 paint
.setStyle(SkPaint::kFill_Style
);
215 canvas
->DrawPath(path
, paint
);
218 void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
219 float device_scale_factor
) {
220 // Redrawing will take care of scale factor change.
223 base::Closure
TrayBubbleContentMask::PrepareForLayerBoundsChange() {
224 return base::Closure();
227 // Custom layout for the bubble-view. Does the default box-layout if there is
228 // enough height. Otherwise, makes sure the bottom rows are visible.
229 class BottomAlignedBoxLayout
: public BoxLayout
{
231 explicit BottomAlignedBoxLayout(TrayBubbleView
* bubble_view
)
232 : BoxLayout(BoxLayout::kVertical
, 0, 0, 0),
233 bubble_view_(bubble_view
) {
236 virtual ~BottomAlignedBoxLayout() {}
239 virtual void Layout(View
* host
) OVERRIDE
{
240 if (host
->height() >= host
->GetPreferredSize().height() ||
241 !bubble_view_
->is_gesture_dragging()) {
242 BoxLayout::Layout(host
);
246 int consumed_height
= 0;
247 for (int i
= host
->child_count() - 1;
248 i
>= 0 && consumed_height
< host
->height(); --i
) {
249 View
* child
= host
->child_at(i
);
250 if (!child
->visible())
252 gfx::Size size
= child
->GetPreferredSize();
253 child
->SetBounds(0, host
->height() - consumed_height
- size
.height(),
254 host
->width(), size
.height());
255 consumed_height
+= size
.height();
259 TrayBubbleView
* bubble_view_
;
261 DISALLOW_COPY_AND_ASSIGN(BottomAlignedBoxLayout
);
264 } // namespace internal
266 using internal::TrayBubbleBorder
;
267 using internal::TrayBubbleContentMask
;
268 using internal::BottomAlignedBoxLayout
;
271 const int TrayBubbleView::InitParams::kArrowDefaultOffset
= -1;
273 TrayBubbleView::InitParams::InitParams(AnchorType anchor_type
,
274 AnchorAlignment anchor_alignment
,
277 : anchor_type(anchor_type
),
278 anchor_alignment(anchor_alignment
),
279 min_width(min_width
),
280 max_width(max_width
),
283 close_on_deactivate(true),
284 arrow_color(SK_ColorBLACK
),
285 first_item_has_no_margin(false),
286 arrow(BubbleBorder::NONE
),
287 arrow_offset(kArrowDefaultOffset
),
288 arrow_paint_type(BubbleBorder::PAINT_NORMAL
),
289 shadow(BubbleBorder::BIG_SHADOW
),
290 arrow_alignment(BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE
) {
294 TrayBubbleView
* TrayBubbleView::Create(gfx::NativeView parent_window
,
297 InitParams
* init_params
) {
298 // Set arrow here so that it can be passed to the BubbleView constructor.
299 if (init_params
->anchor_type
== ANCHOR_TYPE_TRAY
) {
300 if (init_params
->anchor_alignment
== ANCHOR_ALIGNMENT_BOTTOM
) {
301 init_params
->arrow
= base::i18n::IsRTL() ?
302 BubbleBorder::BOTTOM_LEFT
: BubbleBorder::BOTTOM_RIGHT
;
303 } else if (init_params
->anchor_alignment
== ANCHOR_ALIGNMENT_TOP
) {
304 init_params
->arrow
= BubbleBorder::TOP_LEFT
;
305 } else if (init_params
->anchor_alignment
== ANCHOR_ALIGNMENT_LEFT
) {
306 init_params
->arrow
= BubbleBorder::LEFT_BOTTOM
;
308 init_params
->arrow
= BubbleBorder::RIGHT_BOTTOM
;
311 init_params
->arrow
= BubbleBorder::NONE
;
314 return new TrayBubbleView(parent_window
, anchor
, delegate
, *init_params
);
317 TrayBubbleView::TrayBubbleView(gfx::NativeView parent_window
,
320 const InitParams
& init_params
)
321 : BubbleDelegateView(anchor
, init_params
.arrow
),
322 params_(init_params
),
324 preferred_width_(init_params
.min_width
),
325 bubble_border_(NULL
),
326 is_gesture_dragging_(false),
327 mouse_actively_entered_(false) {
328 set_parent_window(parent_window
);
329 set_notify_enter_exit_on_child(true);
330 set_close_on_deactivate(init_params
.close_on_deactivate
);
331 set_margins(gfx::Insets());
332 bubble_border_
= new TrayBubbleBorder(this, GetAnchorView(), params_
);
333 SetPaintToLayer(true);
334 SetFillsBoundsOpaquely(true);
336 bubble_content_mask_
.reset(
337 new TrayBubbleContentMask(bubble_border_
->GetBorderCornerRadius()));
340 TrayBubbleView::~TrayBubbleView() {
341 mouse_watcher_
.reset();
342 // Inform host items (models) that their views are being destroyed.
344 delegate_
->BubbleViewDestroyed();
347 void TrayBubbleView::InitializeAndShowBubble() {
348 // Must occur after call to BubbleDelegateView::CreateBubble().
349 SetAlignment(params_
.arrow_alignment
);
350 bubble_border_
->UpdateArrowOffset();
352 layer()->parent()->SetMaskLayer(bubble_content_mask_
->layer());
355 GetWidget()->GetNativeWindow()->SetEventTargeter(
356 scoped_ptr
<ui::EventTargeter
>(new BubbleWindowTargeter(this)));
360 void TrayBubbleView::UpdateBubble() {
362 bubble_content_mask_
->layer()->SetBounds(layer()->bounds());
363 GetWidget()->GetRootView()->SchedulePaint();
366 void TrayBubbleView::SetMaxHeight(int height
) {
367 params_
.max_height
= height
;
372 void TrayBubbleView::SetWidth(int width
) {
373 width
= std::max(std::min(width
, params_
.max_width
), params_
.min_width
);
374 if (preferred_width_
== width
)
376 preferred_width_
= width
;
381 void TrayBubbleView::SetArrowPaintType(
382 views::BubbleBorder::ArrowPaintType paint_type
) {
383 bubble_border_
->set_paint_arrow(paint_type
);
387 gfx::Insets
TrayBubbleView::GetBorderInsets() const {
388 return bubble_border_
->GetInsets();
391 void TrayBubbleView::Init() {
392 BoxLayout
* layout
= new BottomAlignedBoxLayout(this);
393 layout
->SetDefaultFlex(1);
394 SetLayoutManager(layout
);
397 gfx::Rect
TrayBubbleView::GetAnchorRect() const {
400 return delegate_
->GetAnchorRect(anchor_widget(),
402 params_
.anchor_alignment
);
405 bool TrayBubbleView::CanActivate() const {
406 return params_
.can_activate
;
409 NonClientFrameView
* TrayBubbleView::CreateNonClientFrameView(Widget
* widget
) {
410 BubbleFrameView
* frame
= new BubbleFrameView(margins());
411 frame
->SetBubbleBorder(scoped_ptr
<views::BubbleBorder
>(bubble_border_
));
415 bool TrayBubbleView::WidgetHasHitTestMask() const {
419 void TrayBubbleView::GetWidgetHitTestMask(gfx::Path
* mask
) const {
421 mask
->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
424 gfx::Size
TrayBubbleView::GetPreferredSize() const {
425 return gfx::Size(preferred_width_
, GetHeightForWidth(preferred_width_
));
428 gfx::Size
TrayBubbleView::GetMaximumSize() const {
429 gfx::Size size
= GetPreferredSize();
430 size
.set_width(params_
.max_width
);
434 int TrayBubbleView::GetHeightForWidth(int width
) const {
435 int height
= GetInsets().height();
436 width
= std::max(width
- GetInsets().width(), 0);
437 for (int i
= 0; i
< child_count(); ++i
) {
438 const View
* child
= child_at(i
);
439 if (child
->visible())
440 height
+= child
->GetHeightForWidth(width
);
443 return (params_
.max_height
!= 0) ?
444 std::min(height
, params_
.max_height
) : height
;
447 void TrayBubbleView::OnMouseEntered(const ui::MouseEvent
& event
) {
448 mouse_watcher_
.reset();
449 if (delegate_
&& !(event
.flags() & ui::EF_IS_SYNTHESIZED
)) {
450 // Coming here the user was actively moving the mouse over the bubble and
451 // we inform the delegate that we entered. This will prevent the bubble
453 delegate_
->OnMouseEnteredView();
454 mouse_actively_entered_
= true;
456 // Coming here the bubble got shown and the mouse was 'accidentally' over it
457 // which is not a reason to prevent the bubble to auto close. As such we
458 // do not call the delegate, but wait for the first mouse move within the
459 // bubble. The used MouseWatcher will notify use of a movement and call
460 // |MouseMovedOutOfHost|.
461 mouse_watcher_
.reset(new MouseWatcher(
462 new views::internal::MouseMoveDetectorHost(),
464 // Set the mouse sampling frequency to roughly a frame time so that the user
466 mouse_watcher_
->set_notify_on_exit_time(
467 base::TimeDelta::FromMilliseconds(kFrameTimeInMS
));
468 mouse_watcher_
->Start();
472 void TrayBubbleView::OnMouseExited(const ui::MouseEvent
& event
) {
473 // If there was a mouse watcher waiting for mouse movements we disable it
474 // immediately since we now leave the bubble.
475 mouse_watcher_
.reset();
476 // Do not notify the delegate of an exit if we never told it that we entered.
477 if (delegate_
&& mouse_actively_entered_
)
478 delegate_
->OnMouseExitedView();
481 void TrayBubbleView::GetAccessibleState(ui::AXViewState
* state
) {
482 if (delegate_
&& params_
.can_activate
) {
483 state
->role
= ui::AX_ROLE_WINDOW
;
484 state
->name
= delegate_
->GetAccessibleNameForBubble();
488 void TrayBubbleView::MouseMovedOutOfHost() {
489 // The mouse was accidentally over the bubble when it opened and the AutoClose
490 // logic was not activated. Now that the user did move the mouse we tell the
491 // delegate to disable AutoClose.
492 delegate_
->OnMouseEnteredView();
493 mouse_actively_entered_
= true;
494 mouse_watcher_
->Stop();
497 void TrayBubbleView::ChildPreferredSizeChanged(View
* child
) {
501 void TrayBubbleView::ViewHierarchyChanged(
502 const ViewHierarchyChangedDetails
& details
) {
503 if (details
.is_add
&& details
.child
== this) {
504 details
.parent
->SetPaintToLayer(true);
505 details
.parent
->SetFillsBoundsOpaquely(true);
506 details
.parent
->layer()->SetMasksToBounds(true);