Upstream version 5.34.92.0
[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/aura/window.h"
15 #include "ui/base/accessibility/accessible_view_state.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
66 MouseMoveDetectorHost::MouseMoveDetectorHost() {
67 }
68
69 MouseMoveDetectorHost::~MouseMoveDetectorHost() {
70 }
71
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:
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
94   virtual ~TrayBubbleBorder() {}
95
96   // Overridden from BubbleBorder.
97   // Sets the bubble on top of the anchor when it has no arrow.
98   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
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
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
198 TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius)
199     : layer_(ui::LAYER_TEXTURED),
200       corner_radius_(corner_radius) {
201   layer_.set_delegate(this);
202 }
203
204 TrayBubbleContentMask::~TrayBubbleContentMask() {
205   layer_.set_delegate(NULL);
206 }
207
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
218 void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
219     float device_scale_factor) {
220   // Redrawing will take care of scale factor change.
221 }
222
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:
231   explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view)
232       : BoxLayout(BoxLayout::kVertical, 0, 0, 0),
233         bubble_view_(bubble_view) {
234   }
235
236   virtual ~BottomAlignedBoxLayout() {}
237
238  private:
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
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
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
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_move_with_anchor(true);
331   set_close_on_deactivate(init_params.close_on_deactivate);
332   set_margins(gfx::Insets());
333   bubble_border_ = new TrayBubbleBorder(this, GetAnchorView(), params_);
334   if (get_use_acceleration_when_possible()) {
335     SetPaintToLayer(true);
336     SetFillsBoundsOpaquely(true);
337
338     bubble_content_mask_.reset(
339         new TrayBubbleContentMask(bubble_border_->GetBorderCornerRadius()));
340   }
341 }
342
343 TrayBubbleView::~TrayBubbleView() {
344   mouse_watcher_.reset();
345   // Inform host items (models) that their views are being destroyed.
346   if (delegate_)
347     delegate_->BubbleViewDestroyed();
348 }
349
350 void TrayBubbleView::InitializeAndShowBubble() {
351   // Must occur after call to BubbleDelegateView::CreateBubble().
352   SetAlignment(params_.arrow_alignment);
353   bubble_border_->UpdateArrowOffset();
354
355   if (get_use_acceleration_when_possible())
356     layer()->parent()->SetMaskLayer(bubble_content_mask_->layer());
357
358   GetWidget()->Show();
359   GetWidget()->GetNativeWindow()->set_event_targeter(
360       scoped_ptr<ui::EventTargeter>(new BubbleWindowTargeter(this)));
361   UpdateBubble();
362 }
363
364 void TrayBubbleView::UpdateBubble() {
365   SizeToContents();
366   if (get_use_acceleration_when_possible())
367     bubble_content_mask_->layer()->SetBounds(layer()->bounds());
368   GetWidget()->GetRootView()->SchedulePaint();
369 }
370
371 void TrayBubbleView::SetMaxHeight(int height) {
372   params_.max_height = height;
373   if (GetWidget())
374     SizeToContents();
375 }
376
377 void TrayBubbleView::SetWidth(int width) {
378   width = std::max(std::min(width, params_.max_width), params_.min_width);
379   if (preferred_width_ == width)
380     return;
381   preferred_width_ = width;
382   if (GetWidget())
383     SizeToContents();
384 }
385
386 void TrayBubbleView::SetArrowPaintType(
387     views::BubbleBorder::ArrowPaintType paint_type) {
388   bubble_border_->set_paint_arrow(paint_type);
389 }
390
391 gfx::Insets TrayBubbleView::GetBorderInsets() const {
392   return bubble_border_->GetInsets();
393 }
394
395 void TrayBubbleView::Init() {
396   BoxLayout* layout = new BottomAlignedBoxLayout(this);
397   layout->set_spread_blank_space(true);
398   SetLayoutManager(layout);
399 }
400
401 gfx::Rect TrayBubbleView::GetAnchorRect() {
402   if (!delegate_)
403     return gfx::Rect();
404   return delegate_->GetAnchorRect(anchor_widget(),
405                                   params_.anchor_type,
406                                   params_.anchor_alignment);
407 }
408
409 bool TrayBubbleView::CanActivate() const {
410   return params_.can_activate;
411 }
412
413 NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
414   BubbleFrameView* frame = new BubbleFrameView(margins());
415   frame->SetBubbleBorder(scoped_ptr<views::BubbleBorder>(bubble_border_));
416   return frame;
417 }
418
419 bool TrayBubbleView::WidgetHasHitTestMask() const {
420   return true;
421 }
422
423 void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const {
424   DCHECK(mask);
425   mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
426 }
427
428 gfx::Size TrayBubbleView::GetPreferredSize() {
429   return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
430 }
431
432 gfx::Size TrayBubbleView::GetMaximumSize() {
433   gfx::Size size = GetPreferredSize();
434   size.set_width(params_.max_width);
435   return size;
436 }
437
438 int TrayBubbleView::GetHeightForWidth(int width) {
439   int height = GetInsets().height();
440   width = std::max(width - GetInsets().width(), 0);
441   for (int i = 0; i < child_count(); ++i) {
442     View* child = child_at(i);
443     if (child->visible())
444       height += child->GetHeightForWidth(width);
445   }
446
447   return (params_.max_height != 0) ?
448       std::min(height, params_.max_height) : height;
449 }
450
451 void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
452   mouse_watcher_.reset();
453   if (delegate_ && !(event.flags() & ui::EF_IS_SYNTHESIZED)) {
454     // Coming here the user was actively moving the mouse over the bubble and
455     // we inform the delegate that we entered. This will prevent the bubble
456     // to auto close.
457     delegate_->OnMouseEnteredView();
458     mouse_actively_entered_ = true;
459   } else {
460     // Coming here the bubble got shown and the mouse was 'accidentally' over it
461     // which is not a reason to prevent the bubble to auto close. As such we
462     // do not call the delegate, but wait for the first mouse move within the
463     // bubble. The used MouseWatcher will notify use of a movement and call
464     // |MouseMovedOutOfHost|.
465     mouse_watcher_.reset(new MouseWatcher(
466         new views::internal::MouseMoveDetectorHost(),
467         this));
468     // Set the mouse sampling frequency to roughly a frame time so that the user
469     // cannot see a lag.
470     mouse_watcher_->set_notify_on_exit_time(
471         base::TimeDelta::FromMilliseconds(kFrameTimeInMS));
472     mouse_watcher_->Start();
473   }
474 }
475
476 void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) {
477   // If there was a mouse watcher waiting for mouse movements we disable it
478   // immediately since we now leave the bubble.
479   mouse_watcher_.reset();
480   // Do not notify the delegate of an exit if we never told it that we entered.
481   if (delegate_ && mouse_actively_entered_)
482     delegate_->OnMouseExitedView();
483 }
484
485 void TrayBubbleView::GetAccessibleState(ui::AccessibleViewState* state) {
486   if (delegate_ && params_.can_activate) {
487     state->role = ui::AccessibilityTypes::ROLE_WINDOW;
488     state->name = delegate_->GetAccessibleNameForBubble();
489   }
490 }
491
492 void TrayBubbleView::MouseMovedOutOfHost() {
493   // The mouse was accidentally over the bubble when it opened and the AutoClose
494   // logic was not activated. Now that the user did move the mouse we tell the
495   // delegate to disable AutoClose.
496   delegate_->OnMouseEnteredView();
497   mouse_actively_entered_ = true;
498   mouse_watcher_->Stop();
499 }
500
501 void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
502   SizeToContents();
503 }
504
505 void TrayBubbleView::ViewHierarchyChanged(
506     const ViewHierarchyChangedDetails& details) {
507   if (get_use_acceleration_when_possible() && details.is_add &&
508       details.child == this) {
509     details.parent->SetPaintToLayer(true);
510     details.parent->SetFillsBoundsOpaquely(true);
511     details.parent->layer()->SetMasksToBounds(true);
512   }
513 }
514
515 }  // namespace views