Home | History | Annotate | Download | only in bubble
      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 
     64 MouseMoveDetectorHost::MouseMoveDetectorHost() {
     65 }
     66 
     67 MouseMoveDetectorHost::~MouseMoveDetectorHost() {
     68 }
     69 
     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:
     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.
    177 class 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 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 
    196 TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius)
    197     : layer_(ui::LAYER_TEXTURED),
    198       corner_radius_(corner_radius) {
    199   layer_.set_delegate(this);
    200 }
    201 
    202 TrayBubbleContentMask::~TrayBubbleContentMask() {
    203   layer_.set_delegate(NULL);
    204 }
    205 
    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 
    216 void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
    217     float device_scale_factor) {
    218   // Redrawing will take care of scale factor change.
    219 }
    220 
    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:
    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 
    264 using internal::TrayBubbleBorder;
    265 using internal::TrayBubbleContentMask;
    266 using internal::BottomAlignedBoxLayout;
    267 
    268 // static
    269 const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1;
    270 
    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
    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 
    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 
    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 
    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 
    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 
    367 void TrayBubbleView::SetMaxHeight(int height) {
    368   params_.max_height = height;
    369   if (GetWidget())
    370     SizeToContents();
    371 }
    372 
    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 
    382 void TrayBubbleView::SetArrowPaintType(
    383     views::BubbleBorder::ArrowPaintType paint_type) {
    384   bubble_border_->set_paint_arrow(paint_type);
    385 }
    386 
    387 gfx::Insets TrayBubbleView::GetBorderInsets() const {
    388   return bubble_border_->GetInsets();
    389 }
    390 
    391 void TrayBubbleView::Init() {
    392   BoxLayout* layout = new BottomAlignedBoxLayout(this);
    393   layout->set_spread_blank_space(true);
    394   SetLayoutManager(layout);
    395 }
    396 
    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 
    405 bool TrayBubbleView::CanActivate() const {
    406   return params_.can_activate;
    407 }
    408 
    409 NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
    410   BubbleFrameView* frame = new BubbleFrameView(margins());
    411   frame->SetBubbleBorder(bubble_border_);
    412   return frame;
    413 }
    414 
    415 bool TrayBubbleView::WidgetHasHitTestMask() const {
    416   return true;
    417 }
    418 
    419 void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const {
    420   DCHECK(mask);
    421   mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
    422 }
    423 
    424 gfx::Size TrayBubbleView::GetPreferredSize() {
    425   return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
    426 }
    427 
    428 gfx::Size TrayBubbleView::GetMaximumSize() {
    429   gfx::Size size = GetPreferredSize();
    430   size.set_width(params_.max_width);
    431   return size;
    432 }
    433 
    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 
    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 
    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 
    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 
    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 
    497 void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
    498   SizeToContents();
    499 }
    500 
    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