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
29namespace {
30
31// Inset the arrow a bit from the edge.
32const int kArrowMinOffset = 20;
33const 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.
37const int kArrowOffsetTopBottom = 4;
38const int kArrowOffsetLeft = 9;
39const int kArrowOffsetRight = -5;
40const int kOffsetLeftRightForTopBottomOrientation = 5;
41
42// The sampling time for mouse position changes in ms - which is roughly a frame
43// time.
44const int kFrameTimeInMS = 30;
45}  // namespace
46
47namespace views {
48
49namespace 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.
53class 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
64MouseMoveDetectorHost::MouseMoveDetectorHost() {
65}
66
67MouseMoveDetectorHost::~MouseMoveDetectorHost() {
68}
69
70bool 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.
77class TrayBubbleBorder : public BubbleBorder {
78 public:
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
92  virtual ~TrayBubbleBorder() {}
93
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())) {
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
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.
177class TrayBubbleContentMask : public ui::LayerDelegate {
178 public:
179  explicit TrayBubbleContentMask(int corner_radius);
180  virtual ~TrayBubbleContentMask();
181
182  ui::Layer* layer() { return &layer_; }
183
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;
190
191 private:
192  ui::Layer layer_;
193  int corner_radius_;
194
195  DISALLOW_COPY_AND_ASSIGN(TrayBubbleContentMask);
196};
197
198TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius)
199    : layer_(ui::LAYER_TEXTURED),
200      corner_radius_(corner_radius) {
201  layer_.set_delegate(this);
202}
203
204TrayBubbleContentMask::~TrayBubbleContentMask() {
205  layer_.set_delegate(NULL);
206}
207
208void 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
216void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
217    float device_scale_factor) {
218  // Redrawing will take care of scale factor change.
219}
220
221base::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.
227class BottomAlignedBoxLayout : public BoxLayout {
228 public:
229  explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view)
230      : BoxLayout(BoxLayout::kVertical, 0, 0, 0),
231        bubble_view_(bubble_view) {
232  }
233
234  virtual ~BottomAlignedBoxLayout() {}
235
236 private:
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
264using internal::TrayBubbleBorder;
265using internal::TrayBubbleContentMask;
266using internal::BottomAlignedBoxLayout;
267
268// static
269const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1;
270
271TrayBubbleView::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
292TrayBubbleView* 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
315TrayBubbleView::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
338TrayBubbleView::~TrayBubbleView() {
339  mouse_watcher_.reset();
340  // Inform host items (models) that their views are being destroyed.
341  if (delegate_)
342    delegate_->BubbleViewDestroyed();
343}
344
345void 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
358void TrayBubbleView::UpdateBubble() {
359  SizeToContents();
360  bubble_content_mask_->layer()->SetBounds(layer()->bounds());
361  GetWidget()->GetRootView()->SchedulePaint();
362}
363
364void TrayBubbleView::SetMaxHeight(int height) {
365  params_.max_height = height;
366  if (GetWidget())
367    SizeToContents();
368}
369
370void 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
379void TrayBubbleView::SetArrowPaintType(
380    views::BubbleBorder::ArrowPaintType paint_type) {
381  bubble_border_->set_paint_arrow(paint_type);
382  UpdateBubble();
383}
384
385gfx::Insets TrayBubbleView::GetBorderInsets() const {
386  return bubble_border_->GetInsets();
387}
388
389void TrayBubbleView::Init() {
390  BoxLayout* layout = new BottomAlignedBoxLayout(this);
391  layout->SetDefaultFlex(1);
392  SetLayoutManager(layout);
393}
394
395gfx::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
403bool TrayBubbleView::CanActivate() const {
404  return params_.can_activate;
405}
406
407NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
408  BubbleFrameView* frame = new BubbleFrameView(margins());
409  frame->SetBubbleBorder(scoped_ptr<views::BubbleBorder>(bubble_border_));
410  return frame;
411}
412
413bool TrayBubbleView::WidgetHasHitTestMask() const {
414  return true;
415}
416
417void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const {
418  DCHECK(mask);
419  mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
420}
421
422gfx::Size TrayBubbleView::GetPreferredSize() const {
423  return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
424}
425
426gfx::Size TrayBubbleView::GetMaximumSize() const {
427  gfx::Size size = GetPreferredSize();
428  size.set_width(params_.max_width);
429  return size;
430}
431
432int 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
445void 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
470void 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
479void 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
486void 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
495void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
496  SizeToContents();
497}
498
499void 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