• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/l10n/l10n_util.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/layout/box_layout.h"
26 #include "ui/views/widget/widget.h"
27 
28 namespace {
29 
30 // Inset the arrow a bit from the edge.
31 const int kArrowMinOffset = 20;
32 const 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.
36 const int kArrowOffsetTopBottom = 4;
37 const int kArrowOffsetLeft = 9;
38 const int kArrowOffsetRight = -5;
39 const int kOffsetLeftRightForTopBottomOrientation = 5;
40 
41 // The sampling time for mouse position changes in ms - which is roughly a frame
42 // time.
43 const int kFrameTimeInMS = 30;
44 }  // namespace
45 
46 namespace views {
47 
48 namespace internal {
49 
50 // Detects any mouse movement. This is needed to detect mouse movements by the
51 // user over the bubble if the bubble got created underneath the cursor.
52 class MouseMoveDetectorHost : public MouseWatcherHost {
53  public:
54   MouseMoveDetectorHost();
55   virtual ~MouseMoveDetectorHost();
56 
57   virtual bool Contains(const gfx::Point& screen_point,
58                         MouseEventType type) OVERRIDE;
59  private:
60 
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;
186   virtual void OnDeviceScaleFactorChanged(float device_scale_factor) OVERRIDE;
187   virtual base::Closure PrepareForLayerBoundsChange() OVERRIDE;
188 
189  private:
190   ui::Layer layer_;
191   SkScalar corner_radius_;
192 
193   DISALLOW_COPY_AND_ASSIGN(TrayBubbleContentMask);
194 };
195 
TrayBubbleContentMask(int corner_radius)196 TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius)
197     : layer_(ui::LAYER_TEXTURED),
198       corner_radius_(corner_radius) {
199   layer_.set_delegate(this);
200 }
201 
~TrayBubbleContentMask()202 TrayBubbleContentMask::~TrayBubbleContentMask() {
203   layer_.set_delegate(NULL);
204 }
205 
OnPaintLayer(gfx::Canvas * canvas)206 void TrayBubbleContentMask::OnPaintLayer(gfx::Canvas* canvas) {
207   SkPath path;
208   path.addRoundRect(gfx::RectToSkRect(gfx::Rect(layer()->bounds().size())),
209                     corner_radius_, corner_radius_);
210   SkPaint paint;
211   paint.setAlpha(255);
212   paint.setStyle(SkPaint::kFill_Style);
213   canvas->DrawPath(path, 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_move_with_anchor(true);
329   set_close_on_deactivate(init_params.close_on_deactivate);
330   set_margins(gfx::Insets());
331   bubble_border_ = new TrayBubbleBorder(this, GetAnchorView(), params_);
332   if (get_use_acceleration_when_possible()) {
333     SetPaintToLayer(true);
334     SetFillsBoundsOpaquely(true);
335 
336     bubble_content_mask_.reset(
337         new TrayBubbleContentMask(bubble_border_->GetBorderCornerRadius()));
338   }
339 }
340 
~TrayBubbleView()341 TrayBubbleView::~TrayBubbleView() {
342   mouse_watcher_.reset();
343   // Inform host items (models) that their views are being destroyed.
344   if (delegate_)
345     delegate_->BubbleViewDestroyed();
346 }
347 
InitializeAndShowBubble()348 void TrayBubbleView::InitializeAndShowBubble() {
349   // Must occur after call to BubbleDelegateView::CreateBubble().
350   SetAlignment(params_.arrow_alignment);
351   bubble_border_->UpdateArrowOffset();
352 
353   if (get_use_acceleration_when_possible())
354     layer()->parent()->SetMaskLayer(bubble_content_mask_->layer());
355 
356   GetWidget()->Show();
357   UpdateBubble();
358 }
359 
UpdateBubble()360 void TrayBubbleView::UpdateBubble() {
361   SizeToContents();
362   if (get_use_acceleration_when_possible())
363     bubble_content_mask_->layer()->SetBounds(layer()->bounds());
364   GetWidget()->GetRootView()->SchedulePaint();
365 }
366 
SetMaxHeight(int height)367 void TrayBubbleView::SetMaxHeight(int height) {
368   params_.max_height = height;
369   if (GetWidget())
370     SizeToContents();
371 }
372 
SetWidth(int width)373 void TrayBubbleView::SetWidth(int width) {
374   width = std::max(std::min(width, params_.max_width), params_.min_width);
375   if (preferred_width_ == width)
376     return;
377   preferred_width_ = width;
378   if (GetWidget())
379     SizeToContents();
380 }
381 
SetArrowPaintType(views::BubbleBorder::ArrowPaintType paint_type)382 void TrayBubbleView::SetArrowPaintType(
383     views::BubbleBorder::ArrowPaintType paint_type) {
384   bubble_border_->set_paint_arrow(paint_type);
385 }
386 
GetBorderInsets() const387 gfx::Insets TrayBubbleView::GetBorderInsets() const {
388   return bubble_border_->GetInsets();
389 }
390 
Init()391 void TrayBubbleView::Init() {
392   BoxLayout* layout = new BottomAlignedBoxLayout(this);
393   layout->set_spread_blank_space(true);
394   SetLayoutManager(layout);
395 }
396 
GetAnchorRect()397 gfx::Rect TrayBubbleView::GetAnchorRect() {
398   if (!delegate_)
399     return gfx::Rect();
400   return delegate_->GetAnchorRect(anchor_widget(),
401                                   params_.anchor_type,
402                                   params_.anchor_alignment);
403 }
404 
CanActivate() const405 bool TrayBubbleView::CanActivate() const {
406   return params_.can_activate;
407 }
408 
CreateNonClientFrameView(Widget * widget)409 NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
410   BubbleFrameView* frame = new BubbleFrameView(margins());
411   frame->SetBubbleBorder(bubble_border_);
412   return frame;
413 }
414 
WidgetHasHitTestMask() const415 bool TrayBubbleView::WidgetHasHitTestMask() const {
416   return true;
417 }
418 
GetWidgetHitTestMask(gfx::Path * mask) const419 void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const {
420   DCHECK(mask);
421   mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
422 }
423 
GetPreferredSize()424 gfx::Size TrayBubbleView::GetPreferredSize() {
425   return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
426 }
427 
GetMaximumSize()428 gfx::Size TrayBubbleView::GetMaximumSize() {
429   gfx::Size size = GetPreferredSize();
430   size.set_width(params_.max_width);
431   return size;
432 }
433 
GetHeightForWidth(int width)434 int TrayBubbleView::GetHeightForWidth(int width) {
435   int height = GetInsets().height();
436   width = std::max(width - GetInsets().width(), 0);
437   for (int i = 0; i < child_count(); ++i) {
438     View* child = child_at(i);
439     if (child->visible())
440       height += child->GetHeightForWidth(width);
441   }
442 
443   return (params_.max_height != 0) ?
444       std::min(height, params_.max_height) : height;
445 }
446 
OnMouseEntered(const ui::MouseEvent & event)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
452     // to auto close.
453     delegate_->OnMouseEnteredView();
454     mouse_actively_entered_ = true;
455   } else {
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(),
463         this));
464     // Set the mouse sampling frequency to roughly a frame time so that the user
465     // cannot see a lag.
466     mouse_watcher_->set_notify_on_exit_time(
467         base::TimeDelta::FromMilliseconds(kFrameTimeInMS));
468     mouse_watcher_->Start();
469   }
470 }
471 
OnMouseExited(const ui::MouseEvent & event)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();
479 }
480 
GetAccessibleState(ui::AccessibleViewState * state)481 void TrayBubbleView::GetAccessibleState(ui::AccessibleViewState* state) {
482   if (delegate_ && params_.can_activate) {
483     state->role = ui::AccessibilityTypes::ROLE_WINDOW;
484     state->name = delegate_->GetAccessibleNameForBubble();
485   }
486 }
487 
MouseMovedOutOfHost()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();
495 }
496 
ChildPreferredSizeChanged(View * child)497 void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
498   SizeToContents();
499 }
500 
ViewHierarchyChanged(const ViewHierarchyChangedDetails & details)501 void TrayBubbleView::ViewHierarchyChanged(
502     const ViewHierarchyChangedDetails& details) {
503   if (get_use_acceleration_when_possible() && details.is_add &&
504       details.child == this) {
505     details.parent->SetPaintToLayer(true);
506     details.parent->SetFillsBoundsOpaquely(true);
507     details.parent->layer()->SetMasksToBounds(true);
508   }
509 }
510 
511 }  // namespace views
512