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