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/base/accessibility/accessible_view_state.h"
15#include "ui/base/events/event.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/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/layout/box_layout.h"
26#include "ui/views/widget/widget.h"
27
28namespace {
29
30// Inset the arrow a bit from the edge.
31const int kArrowMinOffset = 20;
32const int kBubbleSpacing = 20;
33
34// The new theme adjusts the menus / bubbles to be flush with the shelf when
35// there is no bubble. These are the offsets which need to be applied.
36const int kArrowOffsetTopBottom = 4;
37const int kArrowOffsetLeft = 9;
38const int kArrowOffsetRight = -5;
39const int kOffsetLeftRightForTopBottomOrientation = 5;
40
41}  // namespace
42
43namespace views {
44
45namespace internal {
46
47// Custom border for TrayBubbleView. Contains special logic for GetBounds()
48// to stack bubbles with no arrows correctly. Also calculates the arrow offset.
49class TrayBubbleBorder : public BubbleBorder {
50 public:
51  TrayBubbleBorder(View* owner,
52                   View* anchor,
53                   TrayBubbleView::InitParams params)
54      : BubbleBorder(params.arrow, params.shadow, params.arrow_color),
55        owner_(owner),
56        anchor_(anchor),
57        tray_arrow_offset_(params.arrow_offset),
58        first_item_has_no_margin_(params.first_item_has_no_margin) {
59    set_alignment(params.arrow_alignment);
60    set_background_color(params.arrow_color);
61    set_paint_arrow(params.arrow_paint_type);
62  }
63
64  virtual ~TrayBubbleBorder() {}
65
66  // Overridden from BubbleBorder.
67  // Sets the bubble on top of the anchor when it has no arrow.
68  virtual gfx::Rect GetBounds(const gfx::Rect& position_relative_to,
69                              const gfx::Size& contents_size) const OVERRIDE {
70    if (has_arrow(arrow())) {
71      gfx::Rect rect =
72          BubbleBorder::GetBounds(position_relative_to, contents_size);
73      if (first_item_has_no_margin_) {
74        if (arrow() == BubbleBorder::BOTTOM_RIGHT ||
75            arrow() == BubbleBorder::BOTTOM_LEFT) {
76          rect.set_y(rect.y() + kArrowOffsetTopBottom);
77          int rtl_factor = base::i18n::IsRTL() ? -1 : 1;
78          rect.set_x(rect.x() +
79                     rtl_factor * kOffsetLeftRightForTopBottomOrientation);
80        } else if (arrow() == BubbleBorder::LEFT_BOTTOM) {
81          rect.set_x(rect.x() + kArrowOffsetLeft);
82        } else if (arrow() == BubbleBorder::RIGHT_BOTTOM) {
83          rect.set_x(rect.x() + kArrowOffsetRight);
84        }
85      }
86      return rect;
87    }
88
89    gfx::Size border_size(contents_size);
90    gfx::Insets insets = GetInsets();
91    border_size.Enlarge(insets.width(), insets.height());
92    const int x = position_relative_to.x() +
93        position_relative_to.width() / 2 - border_size.width() / 2;
94    // Position the bubble on top of the anchor.
95    const int y = position_relative_to.y() - border_size.height()
96        + insets.height() - kBubbleSpacing;
97    return gfx::Rect(x, y, border_size.width(), border_size.height());
98  }
99
100  void UpdateArrowOffset() {
101    int arrow_offset = 0;
102    if (arrow() == BubbleBorder::BOTTOM_RIGHT ||
103        arrow() == BubbleBorder::BOTTOM_LEFT) {
104      // Note: tray_arrow_offset_ is relative to the anchor widget.
105      if (tray_arrow_offset_ ==
106          TrayBubbleView::InitParams::kArrowDefaultOffset) {
107        arrow_offset = kArrowMinOffset;
108      } else {
109        const int width = owner_->GetWidget()->GetContentsView()->width();
110        gfx::Point pt(tray_arrow_offset_, 0);
111        View::ConvertPointToScreen(anchor_->GetWidget()->GetRootView(), &pt);
112        View::ConvertPointFromScreen(owner_->GetWidget()->GetRootView(), &pt);
113        arrow_offset = pt.x();
114        if (arrow() == BubbleBorder::BOTTOM_RIGHT)
115          arrow_offset = width - arrow_offset;
116        arrow_offset = std::max(arrow_offset, kArrowMinOffset);
117      }
118    } else {
119      if (tray_arrow_offset_ ==
120          TrayBubbleView::InitParams::kArrowDefaultOffset) {
121        arrow_offset = kArrowMinOffset;
122      } else {
123        gfx::Point pt(0, tray_arrow_offset_);
124        View::ConvertPointToScreen(anchor_->GetWidget()->GetRootView(), &pt);
125        View::ConvertPointFromScreen(owner_->GetWidget()->GetRootView(), &pt);
126        arrow_offset = pt.y();
127        arrow_offset = std::max(arrow_offset, kArrowMinOffset);
128      }
129    }
130    set_arrow_offset(arrow_offset);
131  }
132
133 private:
134  View* owner_;
135  View* anchor_;
136  const int tray_arrow_offset_;
137
138  // If true the first item should not get any additional spacing against the
139  // anchor (without the bubble tip the bubble should be flush to the shelf).
140  const bool first_item_has_no_margin_;
141
142  DISALLOW_COPY_AND_ASSIGN(TrayBubbleBorder);
143};
144
145// This mask layer clips the bubble's content so that it does not overwrite the
146// rounded bubble corners.
147// TODO(miket): This does not work on Windows. Implement layer masking or
148// alternate solutions if the TrayBubbleView is needed there in the future.
149class TrayBubbleContentMask : public ui::LayerDelegate {
150 public:
151  explicit TrayBubbleContentMask(int corner_radius);
152  virtual ~TrayBubbleContentMask();
153
154  ui::Layer* layer() { return &layer_; }
155
156  // Overridden from LayerDelegate.
157  virtual void OnPaintLayer(gfx::Canvas* canvas) OVERRIDE;
158  virtual void OnDeviceScaleFactorChanged(float device_scale_factor) OVERRIDE;
159  virtual base::Closure PrepareForLayerBoundsChange() OVERRIDE;
160
161 private:
162  ui::Layer layer_;
163  SkScalar corner_radius_;
164
165  DISALLOW_COPY_AND_ASSIGN(TrayBubbleContentMask);
166};
167
168TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius)
169    : layer_(ui::LAYER_TEXTURED),
170      corner_radius_(corner_radius) {
171  layer_.set_delegate(this);
172}
173
174TrayBubbleContentMask::~TrayBubbleContentMask() {
175  layer_.set_delegate(NULL);
176}
177
178void TrayBubbleContentMask::OnPaintLayer(gfx::Canvas* canvas) {
179  SkPath path;
180  path.addRoundRect(gfx::RectToSkRect(gfx::Rect(layer()->bounds().size())),
181                    corner_radius_, corner_radius_);
182  SkPaint paint;
183  paint.setAlpha(255);
184  paint.setStyle(SkPaint::kFill_Style);
185  canvas->DrawPath(path, paint);
186}
187
188void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
189    float device_scale_factor) {
190  // Redrawing will take care of scale factor change.
191}
192
193base::Closure TrayBubbleContentMask::PrepareForLayerBoundsChange() {
194  return base::Closure();
195}
196
197// Custom layout for the bubble-view. Does the default box-layout if there is
198// enough height. Otherwise, makes sure the bottom rows are visible.
199class BottomAlignedBoxLayout : public BoxLayout {
200 public:
201  explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view)
202      : BoxLayout(BoxLayout::kVertical, 0, 0, 0),
203        bubble_view_(bubble_view) {
204  }
205
206  virtual ~BottomAlignedBoxLayout() {}
207
208 private:
209  virtual void Layout(View* host) OVERRIDE {
210    if (host->height() >= host->GetPreferredSize().height() ||
211        !bubble_view_->is_gesture_dragging()) {
212      BoxLayout::Layout(host);
213      return;
214    }
215
216    int consumed_height = 0;
217    for (int i = host->child_count() - 1;
218        i >= 0 && consumed_height < host->height(); --i) {
219      View* child = host->child_at(i);
220      if (!child->visible())
221        continue;
222      gfx::Size size = child->GetPreferredSize();
223      child->SetBounds(0, host->height() - consumed_height - size.height(),
224          host->width(), size.height());
225      consumed_height += size.height();
226    }
227  }
228
229  TrayBubbleView* bubble_view_;
230
231  DISALLOW_COPY_AND_ASSIGN(BottomAlignedBoxLayout);
232};
233
234}  // namespace internal
235
236using internal::TrayBubbleBorder;
237using internal::TrayBubbleContentMask;
238using internal::BottomAlignedBoxLayout;
239
240// static
241const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1;
242
243TrayBubbleView::InitParams::InitParams(AnchorType anchor_type,
244                                       AnchorAlignment anchor_alignment,
245                                       int min_width,
246                                       int max_width)
247    : anchor_type(anchor_type),
248      anchor_alignment(anchor_alignment),
249      min_width(min_width),
250      max_width(max_width),
251      max_height(0),
252      can_activate(false),
253      close_on_deactivate(true),
254      arrow_color(SK_ColorBLACK),
255      first_item_has_no_margin(false),
256      arrow(BubbleBorder::NONE),
257      arrow_offset(kArrowDefaultOffset),
258      arrow_paint_type(BubbleBorder::PAINT_NORMAL),
259      shadow(BubbleBorder::BIG_SHADOW),
260      arrow_alignment(BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE) {
261}
262
263// static
264TrayBubbleView* TrayBubbleView::Create(gfx::NativeView parent_window,
265                                       View* anchor,
266                                       Delegate* delegate,
267                                       InitParams* init_params) {
268  // Set arrow here so that it can be passed to the BubbleView constructor.
269  if (init_params->anchor_type == ANCHOR_TYPE_TRAY) {
270    if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_BOTTOM) {
271      init_params->arrow = base::i18n::IsRTL() ?
272          BubbleBorder::BOTTOM_LEFT : BubbleBorder::BOTTOM_RIGHT;
273    } else if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_TOP) {
274      init_params->arrow = BubbleBorder::TOP_LEFT;
275    } else if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_LEFT) {
276      init_params->arrow = BubbleBorder::LEFT_BOTTOM;
277    } else {
278      init_params->arrow = BubbleBorder::RIGHT_BOTTOM;
279    }
280  } else {
281    init_params->arrow = BubbleBorder::NONE;
282  }
283
284  return new TrayBubbleView(parent_window, anchor, delegate, *init_params);
285}
286
287TrayBubbleView::TrayBubbleView(gfx::NativeView parent_window,
288                               View* anchor,
289                               Delegate* delegate,
290                               const InitParams& init_params)
291    : BubbleDelegateView(anchor, init_params.arrow),
292      params_(init_params),
293      delegate_(delegate),
294      preferred_width_(init_params.min_width),
295      bubble_border_(NULL),
296      is_gesture_dragging_(false) {
297  set_parent_window(parent_window);
298  set_notify_enter_exit_on_child(true);
299  set_close_on_deactivate(init_params.close_on_deactivate);
300  set_margins(gfx::Insets());
301  bubble_border_ = new TrayBubbleBorder(this, anchor_view(), params_);
302  if (get_use_acceleration_when_possible()) {
303    SetPaintToLayer(true);
304    SetFillsBoundsOpaquely(true);
305
306    bubble_content_mask_.reset(
307        new TrayBubbleContentMask(bubble_border_->GetBorderCornerRadius()));
308  }
309}
310
311TrayBubbleView::~TrayBubbleView() {
312  // Inform host items (models) that their views are being destroyed.
313  if (delegate_)
314    delegate_->BubbleViewDestroyed();
315}
316
317void TrayBubbleView::InitializeAndShowBubble() {
318  // Must occur after call to BubbleDelegateView::CreateBubble().
319  SetAlignment(params_.arrow_alignment);
320  bubble_border_->UpdateArrowOffset();
321
322  if (get_use_acceleration_when_possible())
323    layer()->parent()->SetMaskLayer(bubble_content_mask_->layer());
324
325  GetWidget()->Show();
326  UpdateBubble();
327}
328
329void TrayBubbleView::UpdateBubble() {
330  SizeToContents();
331  if (get_use_acceleration_when_possible())
332    bubble_content_mask_->layer()->SetBounds(layer()->bounds());
333  GetWidget()->GetRootView()->SchedulePaint();
334}
335
336void TrayBubbleView::SetMaxHeight(int height) {
337  params_.max_height = height;
338  if (GetWidget())
339    SizeToContents();
340}
341
342void TrayBubbleView::SetWidth(int width) {
343  width = std::max(std::min(width, params_.max_width), params_.min_width);
344  if (preferred_width_ == width)
345    return;
346  preferred_width_ = width;
347  if (GetWidget())
348    SizeToContents();
349}
350
351void TrayBubbleView::SetArrowPaintType(
352    views::BubbleBorder::ArrowPaintType paint_type) {
353  bubble_border_->set_paint_arrow(paint_type);
354}
355
356gfx::Insets TrayBubbleView::GetBorderInsets() const {
357  return bubble_border_->GetInsets();
358}
359
360void TrayBubbleView::Init() {
361  BoxLayout* layout = new BottomAlignedBoxLayout(this);
362  layout->set_spread_blank_space(true);
363  SetLayoutManager(layout);
364}
365
366gfx::Rect TrayBubbleView::GetAnchorRect() {
367  if (!delegate_)
368    return gfx::Rect();
369  return delegate_->GetAnchorRect(anchor_widget(),
370                                  params_.anchor_type,
371                                  params_.anchor_alignment);
372}
373
374bool TrayBubbleView::CanActivate() const {
375  return params_.can_activate;
376}
377
378NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
379  BubbleFrameView* frame = new BubbleFrameView(margins());
380  frame->SetBubbleBorder(bubble_border_);
381  return frame;
382}
383
384bool TrayBubbleView::WidgetHasHitTestMask() const {
385  return true;
386}
387
388void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const {
389  DCHECK(mask);
390  mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
391}
392
393gfx::Size TrayBubbleView::GetPreferredSize() {
394  return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
395}
396
397gfx::Size TrayBubbleView::GetMaximumSize() {
398  gfx::Size size = GetPreferredSize();
399  size.set_width(params_.max_width);
400  return size;
401}
402
403int TrayBubbleView::GetHeightForWidth(int width) {
404  int height = GetInsets().height();
405  width = std::max(width - GetInsets().width(), 0);
406  for (int i = 0; i < child_count(); ++i) {
407    View* child = child_at(i);
408    if (child->visible())
409      height += child->GetHeightForWidth(width);
410  }
411
412  return (params_.max_height != 0) ?
413      std::min(height, params_.max_height) : height;
414}
415
416void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
417  if (delegate_)
418    delegate_->OnMouseEnteredView();
419}
420
421void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) {
422  if (delegate_)
423    delegate_->OnMouseExitedView();
424}
425
426void TrayBubbleView::GetAccessibleState(ui::AccessibleViewState* state) {
427  if (delegate_ && params_.can_activate) {
428    state->role = ui::AccessibilityTypes::ROLE_WINDOW;
429    state->name = delegate_->GetAccessibleNameForBubble();
430  }
431}
432
433void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
434  SizeToContents();
435}
436
437void TrayBubbleView::ViewHierarchyChanged(
438    const ViewHierarchyChangedDetails& details) {
439  if (get_use_acceleration_when_possible() && details.is_add &&
440      details.child == this) {
441    details.parent->SetPaintToLayer(true);
442    details.parent->SetFillsBoundsOpaquely(true);
443    details.parent->layer()->SetMasksToBounds(true);
444  }
445}
446
447}  // namespace views
448