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