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.
4
5 #include "ui/views/bubble/tray_bubble_view.h"
6
7 #include <algorithm>
8
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"
28
29 namespace {
30
31 // Inset the arrow a bit from the edge.
32 const int kArrowMinOffset = 20;
33 const int kBubbleSpacing = 20;
34
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;
41
42 // The sampling time for mouse position changes in ms - which is roughly a frame
43 // time.
44 const int kFrameTimeInMS = 30;
45 } // namespace
46
47 namespace views {
48
49 namespace internal {
50
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 {
54 public:
55 MouseMoveDetectorHost();
56 virtual ~MouseMoveDetectorHost();
57
58 virtual bool Contains(const gfx::Point& screen_point,
59 MouseEventType type) OVERRIDE;
60 private:
61 DISALLOW_COPY_AND_ASSIGN(MouseMoveDetectorHost);
62 };
63
MouseMoveDetectorHost()64 MouseMoveDetectorHost::MouseMoveDetectorHost() {
65 }
66
~MouseMoveDetectorHost()67 MouseMoveDetectorHost::~MouseMoveDetectorHost() {
68 }
69
Contains(const gfx::Point & screen_point,MouseEventType type)70 bool MouseMoveDetectorHost::Contains(const gfx::Point& screen_point,
71 MouseEventType type) {
72 return false;
73 }
74
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 {
78 public:
TrayBubbleBorder(View * owner,View * anchor,TrayBubbleView::InitParams params)79 TrayBubbleBorder(View* owner,
80 View* anchor,
81 TrayBubbleView::InitParams params)
82 : BubbleBorder(params.arrow, params.shadow, params.arrow_color),
83 owner_(owner),
84 anchor_(anchor),
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);
90 }
91
~TrayBubbleBorder()92 virtual ~TrayBubbleBorder() {}
93
94 // Overridden from BubbleBorder.
95 // Sets the bubble on top of the anchor when it has no arrow.
GetBounds(const gfx::Rect & position_relative_to,const gfx::Size & contents_size) const96 virtual gfx::Rect GetBounds(const gfx::Rect& position_relative_to,
97 const gfx::Size& contents_size) const OVERRIDE {
98 if (has_arrow(arrow())) {
99 gfx::Rect rect =
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);
112 }
113 }
114 return rect;
115 }
116
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());
126 }
127
UpdateArrowOffset()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;
136 } else {
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);
145 }
146 } else {
147 if (tray_arrow_offset_ ==
148 TrayBubbleView::InitParams::kArrowDefaultOffset) {
149 arrow_offset = kArrowMinOffset;
150 } else {
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);
156 }
157 }
158 set_arrow_offset(arrow_offset);
159 }
160
161 private:
162 View* owner_;
163 View* anchor_;
164 const int tray_arrow_offset_;
165
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_;
169
170 DISALLOW_COPY_AND_ASSIGN(TrayBubbleBorder);
171 };
172
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 {
178 public:
179 explicit TrayBubbleContentMask(int corner_radius);
180 virtual ~TrayBubbleContentMask();
181
layer()182 ui::Layer* layer() { return &layer_; }
183
184 // Overridden from LayerDelegate.
185 virtual void OnPaintLayer(gfx::Canvas* canvas) OVERRIDE;
OnDelegatedFrameDamage(const gfx::Rect & damage_rect_in_dip)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;
190
191 private:
192 ui::Layer layer_;
193 int corner_radius_;
194
195 DISALLOW_COPY_AND_ASSIGN(TrayBubbleContentMask);
196 };
197
TrayBubbleContentMask(int corner_radius)198 TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius)
199 : layer_(ui::LAYER_TEXTURED),
200 corner_radius_(corner_radius) {
201 layer_.set_delegate(this);
202 }
203
~TrayBubbleContentMask()204 TrayBubbleContentMask::~TrayBubbleContentMask() {
205 layer_.set_delegate(NULL);
206 }
207
OnPaintLayer(gfx::Canvas * canvas)208 void TrayBubbleContentMask::OnPaintLayer(gfx::Canvas* canvas) {
209 SkPaint paint;
210 paint.setAlpha(255);
211 paint.setStyle(SkPaint::kFill_Style);
212 gfx::Rect rect(layer()->bounds().size());
213 canvas->DrawRoundRect(rect, corner_radius_, paint);
214 }
215
OnDeviceScaleFactorChanged(float device_scale_factor)216 void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
217 float device_scale_factor) {
218 // Redrawing will take care of scale factor change.
219 }
220
PrepareForLayerBoundsChange()221 base::Closure TrayBubbleContentMask::PrepareForLayerBoundsChange() {
222 return base::Closure();
223 }
224
225 // Custom layout for the bubble-view. Does the default box-layout if there is
226 // enough height. Otherwise, makes sure the bottom rows are visible.
227 class BottomAlignedBoxLayout : public BoxLayout {
228 public:
BottomAlignedBoxLayout(TrayBubbleView * bubble_view)229 explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view)
230 : BoxLayout(BoxLayout::kVertical, 0, 0, 0),
231 bubble_view_(bubble_view) {
232 }
233
~BottomAlignedBoxLayout()234 virtual ~BottomAlignedBoxLayout() {}
235
236 private:
Layout(View * host)237 virtual void Layout(View* host) OVERRIDE {
238 if (host->height() >= host->GetPreferredSize().height() ||
239 !bubble_view_->is_gesture_dragging()) {
240 BoxLayout::Layout(host);
241 return;
242 }
243
244 int consumed_height = 0;
245 for (int i = host->child_count() - 1;
246 i >= 0 && consumed_height < host->height(); --i) {
247 View* child = host->child_at(i);
248 if (!child->visible())
249 continue;
250 gfx::Size size = child->GetPreferredSize();
251 child->SetBounds(0, host->height() - consumed_height - size.height(),
252 host->width(), size.height());
253 consumed_height += size.height();
254 }
255 }
256
257 TrayBubbleView* bubble_view_;
258
259 DISALLOW_COPY_AND_ASSIGN(BottomAlignedBoxLayout);
260 };
261
262 } // namespace internal
263
264 using internal::TrayBubbleBorder;
265 using internal::TrayBubbleContentMask;
266 using internal::BottomAlignedBoxLayout;
267
268 // static
269 const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1;
270
InitParams(AnchorType anchor_type,AnchorAlignment anchor_alignment,int min_width,int max_width)271 TrayBubbleView::InitParams::InitParams(AnchorType anchor_type,
272 AnchorAlignment anchor_alignment,
273 int min_width,
274 int max_width)
275 : anchor_type(anchor_type),
276 anchor_alignment(anchor_alignment),
277 min_width(min_width),
278 max_width(max_width),
279 max_height(0),
280 can_activate(false),
281 close_on_deactivate(true),
282 arrow_color(SK_ColorBLACK),
283 first_item_has_no_margin(false),
284 arrow(BubbleBorder::NONE),
285 arrow_offset(kArrowDefaultOffset),
286 arrow_paint_type(BubbleBorder::PAINT_NORMAL),
287 shadow(BubbleBorder::BIG_SHADOW),
288 arrow_alignment(BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE) {
289 }
290
291 // static
Create(gfx::NativeView parent_window,View * anchor,Delegate * delegate,InitParams * init_params)292 TrayBubbleView* TrayBubbleView::Create(gfx::NativeView parent_window,
293 View* anchor,
294 Delegate* delegate,
295 InitParams* init_params) {
296 // Set arrow here so that it can be passed to the BubbleView constructor.
297 if (init_params->anchor_type == ANCHOR_TYPE_TRAY) {
298 if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_BOTTOM) {
299 init_params->arrow = base::i18n::IsRTL() ?
300 BubbleBorder::BOTTOM_LEFT : BubbleBorder::BOTTOM_RIGHT;
301 } else if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_TOP) {
302 init_params->arrow = BubbleBorder::TOP_LEFT;
303 } else if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_LEFT) {
304 init_params->arrow = BubbleBorder::LEFT_BOTTOM;
305 } else {
306 init_params->arrow = BubbleBorder::RIGHT_BOTTOM;
307 }
308 } else {
309 init_params->arrow = BubbleBorder::NONE;
310 }
311
312 return new TrayBubbleView(parent_window, anchor, delegate, *init_params);
313 }
314
TrayBubbleView(gfx::NativeView parent_window,View * anchor,Delegate * delegate,const InitParams & init_params)315 TrayBubbleView::TrayBubbleView(gfx::NativeView parent_window,
316 View* anchor,
317 Delegate* delegate,
318 const InitParams& init_params)
319 : BubbleDelegateView(anchor, init_params.arrow),
320 params_(init_params),
321 delegate_(delegate),
322 preferred_width_(init_params.min_width),
323 bubble_border_(NULL),
324 is_gesture_dragging_(false),
325 mouse_actively_entered_(false) {
326 set_parent_window(parent_window);
327 set_notify_enter_exit_on_child(true);
328 set_close_on_deactivate(init_params.close_on_deactivate);
329 set_margins(gfx::Insets());
330 bubble_border_ = new TrayBubbleBorder(this, GetAnchorView(), params_);
331 SetPaintToLayer(true);
332 SetFillsBoundsOpaquely(true);
333
334 bubble_content_mask_.reset(
335 new TrayBubbleContentMask(bubble_border_->GetBorderCornerRadius()));
336 }
337
~TrayBubbleView()338 TrayBubbleView::~TrayBubbleView() {
339 mouse_watcher_.reset();
340 // Inform host items (models) that their views are being destroyed.
341 if (delegate_)
342 delegate_->BubbleViewDestroyed();
343 }
344
InitializeAndShowBubble()345 void TrayBubbleView::InitializeAndShowBubble() {
346 // Must occur after call to BubbleDelegateView::CreateBubble().
347 SetAlignment(params_.arrow_alignment);
348 bubble_border_->UpdateArrowOffset();
349
350 layer()->parent()->SetMaskLayer(bubble_content_mask_->layer());
351
352 GetWidget()->Show();
353 GetWidget()->GetNativeWindow()->SetEventTargeter(
354 scoped_ptr<ui::EventTargeter>(new BubbleWindowTargeter(this)));
355 UpdateBubble();
356 }
357
UpdateBubble()358 void TrayBubbleView::UpdateBubble() {
359 SizeToContents();
360 bubble_content_mask_->layer()->SetBounds(layer()->bounds());
361 GetWidget()->GetRootView()->SchedulePaint();
362 }
363
SetMaxHeight(int height)364 void TrayBubbleView::SetMaxHeight(int height) {
365 params_.max_height = height;
366 if (GetWidget())
367 SizeToContents();
368 }
369
SetWidth(int width)370 void TrayBubbleView::SetWidth(int width) {
371 width = std::max(std::min(width, params_.max_width), params_.min_width);
372 if (preferred_width_ == width)
373 return;
374 preferred_width_ = width;
375 if (GetWidget())
376 SizeToContents();
377 }
378
SetArrowPaintType(views::BubbleBorder::ArrowPaintType paint_type)379 void TrayBubbleView::SetArrowPaintType(
380 views::BubbleBorder::ArrowPaintType paint_type) {
381 bubble_border_->set_paint_arrow(paint_type);
382 UpdateBubble();
383 }
384
GetBorderInsets() const385 gfx::Insets TrayBubbleView::GetBorderInsets() const {
386 return bubble_border_->GetInsets();
387 }
388
Init()389 void TrayBubbleView::Init() {
390 BoxLayout* layout = new BottomAlignedBoxLayout(this);
391 layout->SetDefaultFlex(1);
392 SetLayoutManager(layout);
393 }
394
GetAnchorRect() const395 gfx::Rect TrayBubbleView::GetAnchorRect() const {
396 if (!delegate_)
397 return gfx::Rect();
398 return delegate_->GetAnchorRect(anchor_widget(),
399 params_.anchor_type,
400 params_.anchor_alignment);
401 }
402
CanActivate() const403 bool TrayBubbleView::CanActivate() const {
404 return params_.can_activate;
405 }
406
CreateNonClientFrameView(Widget * widget)407 NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
408 BubbleFrameView* frame = new BubbleFrameView(margins());
409 frame->SetBubbleBorder(scoped_ptr<views::BubbleBorder>(bubble_border_));
410 return frame;
411 }
412
WidgetHasHitTestMask() const413 bool TrayBubbleView::WidgetHasHitTestMask() const {
414 return true;
415 }
416
GetWidgetHitTestMask(gfx::Path * mask) const417 void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const {
418 DCHECK(mask);
419 mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
420 }
421
GetPreferredSize() const422 gfx::Size TrayBubbleView::GetPreferredSize() const {
423 return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
424 }
425
GetMaximumSize() const426 gfx::Size TrayBubbleView::GetMaximumSize() const {
427 gfx::Size size = GetPreferredSize();
428 size.set_width(params_.max_width);
429 return size;
430 }
431
GetHeightForWidth(int width) const432 int TrayBubbleView::GetHeightForWidth(int width) const {
433 int height = GetInsets().height();
434 width = std::max(width - GetInsets().width(), 0);
435 for (int i = 0; i < child_count(); ++i) {
436 const View* child = child_at(i);
437 if (child->visible())
438 height += child->GetHeightForWidth(width);
439 }
440
441 return (params_.max_height != 0) ?
442 std::min(height, params_.max_height) : height;
443 }
444
OnMouseEntered(const ui::MouseEvent & event)445 void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
446 mouse_watcher_.reset();
447 if (delegate_ && !(event.flags() & ui::EF_IS_SYNTHESIZED)) {
448 // Coming here the user was actively moving the mouse over the bubble and
449 // we inform the delegate that we entered. This will prevent the bubble
450 // to auto close.
451 delegate_->OnMouseEnteredView();
452 mouse_actively_entered_ = true;
453 } else {
454 // Coming here the bubble got shown and the mouse was 'accidentally' over it
455 // which is not a reason to prevent the bubble to auto close. As such we
456 // do not call the delegate, but wait for the first mouse move within the
457 // bubble. The used MouseWatcher will notify use of a movement and call
458 // |MouseMovedOutOfHost|.
459 mouse_watcher_.reset(new MouseWatcher(
460 new views::internal::MouseMoveDetectorHost(),
461 this));
462 // Set the mouse sampling frequency to roughly a frame time so that the user
463 // cannot see a lag.
464 mouse_watcher_->set_notify_on_exit_time(
465 base::TimeDelta::FromMilliseconds(kFrameTimeInMS));
466 mouse_watcher_->Start();
467 }
468 }
469
OnMouseExited(const ui::MouseEvent & event)470 void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) {
471 // If there was a mouse watcher waiting for mouse movements we disable it
472 // immediately since we now leave the bubble.
473 mouse_watcher_.reset();
474 // Do not notify the delegate of an exit if we never told it that we entered.
475 if (delegate_ && mouse_actively_entered_)
476 delegate_->OnMouseExitedView();
477 }
478
GetAccessibleState(ui::AXViewState * state)479 void TrayBubbleView::GetAccessibleState(ui::AXViewState* state) {
480 if (delegate_ && params_.can_activate) {
481 state->role = ui::AX_ROLE_WINDOW;
482 state->name = delegate_->GetAccessibleNameForBubble();
483 }
484 }
485
MouseMovedOutOfHost()486 void TrayBubbleView::MouseMovedOutOfHost() {
487 // The mouse was accidentally over the bubble when it opened and the AutoClose
488 // logic was not activated. Now that the user did move the mouse we tell the
489 // delegate to disable AutoClose.
490 delegate_->OnMouseEnteredView();
491 mouse_actively_entered_ = true;
492 mouse_watcher_->Stop();
493 }
494
ChildPreferredSizeChanged(View * child)495 void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
496 SizeToContents();
497 }
498
ViewHierarchyChanged(const ViewHierarchyChangedDetails & details)499 void TrayBubbleView::ViewHierarchyChanged(
500 const ViewHierarchyChangedDetails& details) {
501 if (details.is_add && details.child == this) {
502 details.parent->SetPaintToLayer(true);
503 details.parent->SetFillsBoundsOpaquely(true);
504 details.parent->layer()->SetMasksToBounds(true);
505 }
506 }
507
508 } // namespace views
509