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/base/l10n/l10n_util.h"
17 #include "ui/compositor/layer.h"
18 #include "ui/compositor/layer_delegate.h"
19 #include "ui/events/event.h"
20 #include "ui/gfx/canvas.h"
21 #include "ui/gfx/insets.h"
22 #include "ui/gfx/path.h"
23 #include "ui/gfx/rect.h"
24 #include "ui/gfx/skia_util.h"
25 #include "ui/views/bubble/bubble_frame_view.h"
26 #include "ui/views/bubble/bubble_window_targeter.h"
27 #include "ui/views/layout/box_layout.h"
28 #include "ui/views/widget/widget.h"
29
30 namespace {
31
32 // Inset the arrow a bit from the edge.
33 const int kArrowMinOffset = 20;
34 const int kBubbleSpacing = 20;
35
36 // The new theme adjusts the menus / bubbles to be flush with the shelf when
37 // there is no bubble. These are the offsets which need to be applied.
38 const int kArrowOffsetTopBottom = 4;
39 const int kArrowOffsetLeft = 9;
40 const int kArrowOffsetRight = -5;
41 const int kOffsetLeftRightForTopBottomOrientation = 5;
42
43 // The sampling time for mouse position changes in ms - which is roughly a frame
44 // time.
45 const int kFrameTimeInMS = 30;
46 } // namespace
47
48 namespace views {
49
50 namespace internal {
51
52 // Detects any mouse movement. This is needed to detect mouse movements by the
53 // user over the bubble if the bubble got created underneath the cursor.
54 class MouseMoveDetectorHost : public MouseWatcherHost {
55 public:
56 MouseMoveDetectorHost();
57 virtual ~MouseMoveDetectorHost();
58
59 virtual bool Contains(const gfx::Point& screen_point,
60 MouseEventType type) OVERRIDE;
61 private:
62
63 DISALLOW_COPY_AND_ASSIGN(MouseMoveDetectorHost);
64 };
65
MouseMoveDetectorHost()66 MouseMoveDetectorHost::MouseMoveDetectorHost() {
67 }
68
~MouseMoveDetectorHost()69 MouseMoveDetectorHost::~MouseMoveDetectorHost() {
70 }
71
Contains(const gfx::Point & screen_point,MouseEventType type)72 bool MouseMoveDetectorHost::Contains(const gfx::Point& screen_point,
73 MouseEventType type) {
74 return false;
75 }
76
77 // Custom border for TrayBubbleView. Contains special logic for GetBounds()
78 // to stack bubbles with no arrows correctly. Also calculates the arrow offset.
79 class TrayBubbleBorder : public BubbleBorder {
80 public:
TrayBubbleBorder(View * owner,View * anchor,TrayBubbleView::InitParams params)81 TrayBubbleBorder(View* owner,
82 View* anchor,
83 TrayBubbleView::InitParams params)
84 : BubbleBorder(params.arrow, params.shadow, params.arrow_color),
85 owner_(owner),
86 anchor_(anchor),
87 tray_arrow_offset_(params.arrow_offset),
88 first_item_has_no_margin_(params.first_item_has_no_margin) {
89 set_alignment(params.arrow_alignment);
90 set_background_color(params.arrow_color);
91 set_paint_arrow(params.arrow_paint_type);
92 }
93
~TrayBubbleBorder()94 virtual ~TrayBubbleBorder() {}
95
96 // Overridden from BubbleBorder.
97 // 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) const98 virtual gfx::Rect GetBounds(const gfx::Rect& position_relative_to,
99 const gfx::Size& contents_size) const OVERRIDE {
100 if (has_arrow(arrow())) {
101 gfx::Rect rect =
102 BubbleBorder::GetBounds(position_relative_to, contents_size);
103 if (first_item_has_no_margin_) {
104 if (arrow() == BubbleBorder::BOTTOM_RIGHT ||
105 arrow() == BubbleBorder::BOTTOM_LEFT) {
106 rect.set_y(rect.y() + kArrowOffsetTopBottom);
107 int rtl_factor = base::i18n::IsRTL() ? -1 : 1;
108 rect.set_x(rect.x() +
109 rtl_factor * kOffsetLeftRightForTopBottomOrientation);
110 } else if (arrow() == BubbleBorder::LEFT_BOTTOM) {
111 rect.set_x(rect.x() + kArrowOffsetLeft);
112 } else if (arrow() == BubbleBorder::RIGHT_BOTTOM) {
113 rect.set_x(rect.x() + kArrowOffsetRight);
114 }
115 }
116 return rect;
117 }
118
119 gfx::Size border_size(contents_size);
120 gfx::Insets insets = GetInsets();
121 border_size.Enlarge(insets.width(), insets.height());
122 const int x = position_relative_to.x() +
123 position_relative_to.width() / 2 - border_size.width() / 2;
124 // Position the bubble on top of the anchor.
125 const int y = position_relative_to.y() - border_size.height() +
126 insets.height() - kBubbleSpacing;
127 return gfx::Rect(x, y, border_size.width(), border_size.height());
128 }
129
UpdateArrowOffset()130 void UpdateArrowOffset() {
131 int arrow_offset = 0;
132 if (arrow() == BubbleBorder::BOTTOM_RIGHT ||
133 arrow() == BubbleBorder::BOTTOM_LEFT) {
134 // Note: tray_arrow_offset_ is relative to the anchor widget.
135 if (tray_arrow_offset_ ==
136 TrayBubbleView::InitParams::kArrowDefaultOffset) {
137 arrow_offset = kArrowMinOffset;
138 } else {
139 const int width = owner_->GetWidget()->GetContentsView()->width();
140 gfx::Point pt(tray_arrow_offset_, 0);
141 View::ConvertPointToScreen(anchor_->GetWidget()->GetRootView(), &pt);
142 View::ConvertPointFromScreen(owner_->GetWidget()->GetRootView(), &pt);
143 arrow_offset = pt.x();
144 if (arrow() == BubbleBorder::BOTTOM_RIGHT)
145 arrow_offset = width - arrow_offset;
146 arrow_offset = std::max(arrow_offset, kArrowMinOffset);
147 }
148 } else {
149 if (tray_arrow_offset_ ==
150 TrayBubbleView::InitParams::kArrowDefaultOffset) {
151 arrow_offset = kArrowMinOffset;
152 } else {
153 gfx::Point pt(0, tray_arrow_offset_);
154 View::ConvertPointToScreen(anchor_->GetWidget()->GetRootView(), &pt);
155 View::ConvertPointFromScreen(owner_->GetWidget()->GetRootView(), &pt);
156 arrow_offset = pt.y();
157 arrow_offset = std::max(arrow_offset, kArrowMinOffset);
158 }
159 }
160 set_arrow_offset(arrow_offset);
161 }
162
163 private:
164 View* owner_;
165 View* anchor_;
166 const int tray_arrow_offset_;
167
168 // If true the first item should not get any additional spacing against the
169 // anchor (without the bubble tip the bubble should be flush to the shelf).
170 const bool first_item_has_no_margin_;
171
172 DISALLOW_COPY_AND_ASSIGN(TrayBubbleBorder);
173 };
174
175 // This mask layer clips the bubble's content so that it does not overwrite the
176 // rounded bubble corners.
177 // TODO(miket): This does not work on Windows. Implement layer masking or
178 // alternate solutions if the TrayBubbleView is needed there in the future.
179 class TrayBubbleContentMask : public ui::LayerDelegate {
180 public:
181 explicit TrayBubbleContentMask(int corner_radius);
182 virtual ~TrayBubbleContentMask();
183
layer()184 ui::Layer* layer() { return &layer_; }
185
186 // Overridden from LayerDelegate.
187 virtual void OnPaintLayer(gfx::Canvas* canvas) OVERRIDE;
188 virtual void OnDeviceScaleFactorChanged(float device_scale_factor) OVERRIDE;
189 virtual base::Closure PrepareForLayerBoundsChange() OVERRIDE;
190
191 private:
192 ui::Layer layer_;
193 SkScalar 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 SkPath path;
210 path.addRoundRect(gfx::RectToSkRect(gfx::Rect(layer()->bounds().size())),
211 corner_radius_, corner_radius_);
212 SkPaint paint;
213 paint.setAlpha(255);
214 paint.setStyle(SkPaint::kFill_Style);
215 canvas->DrawPath(path, paint);
216 }
217
OnDeviceScaleFactorChanged(float device_scale_factor)218 void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
219 float device_scale_factor) {
220 // Redrawing will take care of scale factor change.
221 }
222
PrepareForLayerBoundsChange()223 base::Closure TrayBubbleContentMask::PrepareForLayerBoundsChange() {
224 return base::Closure();
225 }
226
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 {
230 public:
BottomAlignedBoxLayout(TrayBubbleView * bubble_view)231 explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view)
232 : BoxLayout(BoxLayout::kVertical, 0, 0, 0),
233 bubble_view_(bubble_view) {
234 }
235
~BottomAlignedBoxLayout()236 virtual ~BottomAlignedBoxLayout() {}
237
238 private:
Layout(View * host)239 virtual void Layout(View* host) OVERRIDE {
240 if (host->height() >= host->GetPreferredSize().height() ||
241 !bubble_view_->is_gesture_dragging()) {
242 BoxLayout::Layout(host);
243 return;
244 }
245
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())
251 continue;
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();
256 }
257 }
258
259 TrayBubbleView* bubble_view_;
260
261 DISALLOW_COPY_AND_ASSIGN(BottomAlignedBoxLayout);
262 };
263
264 } // namespace internal
265
266 using internal::TrayBubbleBorder;
267 using internal::TrayBubbleContentMask;
268 using internal::BottomAlignedBoxLayout;
269
270 // static
271 const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1;
272
InitParams(AnchorType anchor_type,AnchorAlignment anchor_alignment,int min_width,int max_width)273 TrayBubbleView::InitParams::InitParams(AnchorType anchor_type,
274 AnchorAlignment anchor_alignment,
275 int min_width,
276 int max_width)
277 : anchor_type(anchor_type),
278 anchor_alignment(anchor_alignment),
279 min_width(min_width),
280 max_width(max_width),
281 max_height(0),
282 can_activate(false),
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) {
291 }
292
293 // static
Create(gfx::NativeView parent_window,View * anchor,Delegate * delegate,InitParams * init_params)294 TrayBubbleView* TrayBubbleView::Create(gfx::NativeView parent_window,
295 View* anchor,
296 Delegate* delegate,
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;
307 } else {
308 init_params->arrow = BubbleBorder::RIGHT_BOTTOM;
309 }
310 } else {
311 init_params->arrow = BubbleBorder::NONE;
312 }
313
314 return new TrayBubbleView(parent_window, anchor, delegate, *init_params);
315 }
316
TrayBubbleView(gfx::NativeView parent_window,View * anchor,Delegate * delegate,const InitParams & init_params)317 TrayBubbleView::TrayBubbleView(gfx::NativeView parent_window,
318 View* anchor,
319 Delegate* delegate,
320 const InitParams& init_params)
321 : BubbleDelegateView(anchor, init_params.arrow),
322 params_(init_params),
323 delegate_(delegate),
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);
335
336 bubble_content_mask_.reset(
337 new TrayBubbleContentMask(bubble_border_->GetBorderCornerRadius()));
338 }
339
~TrayBubbleView()340 TrayBubbleView::~TrayBubbleView() {
341 mouse_watcher_.reset();
342 // Inform host items (models) that their views are being destroyed.
343 if (delegate_)
344 delegate_->BubbleViewDestroyed();
345 }
346
InitializeAndShowBubble()347 void TrayBubbleView::InitializeAndShowBubble() {
348 // Must occur after call to BubbleDelegateView::CreateBubble().
349 SetAlignment(params_.arrow_alignment);
350 bubble_border_->UpdateArrowOffset();
351
352 layer()->parent()->SetMaskLayer(bubble_content_mask_->layer());
353
354 GetWidget()->Show();
355 GetWidget()->GetNativeWindow()->SetEventTargeter(
356 scoped_ptr<ui::EventTargeter>(new BubbleWindowTargeter(this)));
357 UpdateBubble();
358 }
359
UpdateBubble()360 void TrayBubbleView::UpdateBubble() {
361 SizeToContents();
362 bubble_content_mask_->layer()->SetBounds(layer()->bounds());
363 GetWidget()->GetRootView()->SchedulePaint();
364 }
365
SetMaxHeight(int height)366 void TrayBubbleView::SetMaxHeight(int height) {
367 params_.max_height = height;
368 if (GetWidget())
369 SizeToContents();
370 }
371
SetWidth(int width)372 void TrayBubbleView::SetWidth(int width) {
373 width = std::max(std::min(width, params_.max_width), params_.min_width);
374 if (preferred_width_ == width)
375 return;
376 preferred_width_ = width;
377 if (GetWidget())
378 SizeToContents();
379 }
380
SetArrowPaintType(views::BubbleBorder::ArrowPaintType paint_type)381 void TrayBubbleView::SetArrowPaintType(
382 views::BubbleBorder::ArrowPaintType paint_type) {
383 bubble_border_->set_paint_arrow(paint_type);
384 }
385
GetBorderInsets() const386 gfx::Insets TrayBubbleView::GetBorderInsets() const {
387 return bubble_border_->GetInsets();
388 }
389
Init()390 void TrayBubbleView::Init() {
391 BoxLayout* layout = new BottomAlignedBoxLayout(this);
392 layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_FILL);
393 SetLayoutManager(layout);
394 }
395
GetAnchorRect() const396 gfx::Rect TrayBubbleView::GetAnchorRect() const {
397 if (!delegate_)
398 return gfx::Rect();
399 return delegate_->GetAnchorRect(anchor_widget(),
400 params_.anchor_type,
401 params_.anchor_alignment);
402 }
403
CanActivate() const404 bool TrayBubbleView::CanActivate() const {
405 return params_.can_activate;
406 }
407
CreateNonClientFrameView(Widget * widget)408 NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
409 BubbleFrameView* frame = new BubbleFrameView(margins());
410 frame->SetBubbleBorder(scoped_ptr<views::BubbleBorder>(bubble_border_));
411 return frame;
412 }
413
WidgetHasHitTestMask() const414 bool TrayBubbleView::WidgetHasHitTestMask() const {
415 return true;
416 }
417
GetWidgetHitTestMask(gfx::Path * mask) const418 void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const {
419 DCHECK(mask);
420 mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
421 }
422
GetPreferredSize() const423 gfx::Size TrayBubbleView::GetPreferredSize() const {
424 return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
425 }
426
GetMaximumSize() const427 gfx::Size TrayBubbleView::GetMaximumSize() const {
428 gfx::Size size = GetPreferredSize();
429 size.set_width(params_.max_width);
430 return size;
431 }
432
GetHeightForWidth(int width) const433 int TrayBubbleView::GetHeightForWidth(int width) const {
434 int height = GetInsets().height();
435 width = std::max(width - GetInsets().width(), 0);
436 for (int i = 0; i < child_count(); ++i) {
437 const View* child = child_at(i);
438 if (child->visible())
439 height += child->GetHeightForWidth(width);
440 }
441
442 return (params_.max_height != 0) ?
443 std::min(height, params_.max_height) : height;
444 }
445
OnMouseEntered(const ui::MouseEvent & event)446 void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
447 mouse_watcher_.reset();
448 if (delegate_ && !(event.flags() & ui::EF_IS_SYNTHESIZED)) {
449 // Coming here the user was actively moving the mouse over the bubble and
450 // we inform the delegate that we entered. This will prevent the bubble
451 // to auto close.
452 delegate_->OnMouseEnteredView();
453 mouse_actively_entered_ = true;
454 } else {
455 // Coming here the bubble got shown and the mouse was 'accidentally' over it
456 // which is not a reason to prevent the bubble to auto close. As such we
457 // do not call the delegate, but wait for the first mouse move within the
458 // bubble. The used MouseWatcher will notify use of a movement and call
459 // |MouseMovedOutOfHost|.
460 mouse_watcher_.reset(new MouseWatcher(
461 new views::internal::MouseMoveDetectorHost(),
462 this));
463 // Set the mouse sampling frequency to roughly a frame time so that the user
464 // cannot see a lag.
465 mouse_watcher_->set_notify_on_exit_time(
466 base::TimeDelta::FromMilliseconds(kFrameTimeInMS));
467 mouse_watcher_->Start();
468 }
469 }
470
OnMouseExited(const ui::MouseEvent & event)471 void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) {
472 // If there was a mouse watcher waiting for mouse movements we disable it
473 // immediately since we now leave the bubble.
474 mouse_watcher_.reset();
475 // Do not notify the delegate of an exit if we never told it that we entered.
476 if (delegate_ && mouse_actively_entered_)
477 delegate_->OnMouseExitedView();
478 }
479
GetAccessibleState(ui::AXViewState * state)480 void TrayBubbleView::GetAccessibleState(ui::AXViewState* state) {
481 if (delegate_ && params_.can_activate) {
482 state->role = ui::AX_ROLE_WINDOW;
483 state->name = delegate_->GetAccessibleNameForBubble();
484 }
485 }
486
MouseMovedOutOfHost()487 void TrayBubbleView::MouseMovedOutOfHost() {
488 // The mouse was accidentally over the bubble when it opened and the AutoClose
489 // logic was not activated. Now that the user did move the mouse we tell the
490 // delegate to disable AutoClose.
491 delegate_->OnMouseEnteredView();
492 mouse_actively_entered_ = true;
493 mouse_watcher_->Stop();
494 }
495
ChildPreferredSizeChanged(View * child)496 void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
497 SizeToContents();
498 }
499
ViewHierarchyChanged(const ViewHierarchyChangedDetails & details)500 void TrayBubbleView::ViewHierarchyChanged(
501 const ViewHierarchyChangedDetails& details) {
502 if (details.is_add && details.child == this) {
503 details.parent->SetPaintToLayer(true);
504 details.parent->SetFillsBoundsOpaquely(true);
505 details.parent->layer()->SetMasksToBounds(true);
506 }
507 }
508
509 } // namespace views
510