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