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 28namespace { 29 30// Inset the arrow a bit from the edge. 31const int kArrowMinOffset = 20; 32const 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. 36const int kArrowOffsetTopBottom = 4; 37const int kArrowOffsetLeft = 9; 38const int kArrowOffsetRight = -5; 39const int kOffsetLeftRightForTopBottomOrientation = 5; 40 41} // namespace 42 43namespace views { 44 45namespace 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. 49class 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. 149class 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 168TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius) 169 : layer_(ui::LAYER_TEXTURED), 170 corner_radius_(corner_radius) { 171 layer_.set_delegate(this); 172} 173 174TrayBubbleContentMask::~TrayBubbleContentMask() { 175 layer_.set_delegate(NULL); 176} 177 178void 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 188void TrayBubbleContentMask::OnDeviceScaleFactorChanged( 189 float device_scale_factor) { 190 // Redrawing will take care of scale factor change. 191} 192 193base::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. 199class 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 236using internal::TrayBubbleBorder; 237using internal::TrayBubbleContentMask; 238using internal::BottomAlignedBoxLayout; 239 240// static 241const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1; 242 243TrayBubbleView::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 264TrayBubbleView* 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 287TrayBubbleView::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 311TrayBubbleView::~TrayBubbleView() { 312 // Inform host items (models) that their views are being destroyed. 313 if (delegate_) 314 delegate_->BubbleViewDestroyed(); 315} 316 317void 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 329void 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 336void TrayBubbleView::SetMaxHeight(int height) { 337 params_.max_height = height; 338 if (GetWidget()) 339 SizeToContents(); 340} 341 342void 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 351void TrayBubbleView::SetArrowPaintType( 352 views::BubbleBorder::ArrowPaintType paint_type) { 353 bubble_border_->set_paint_arrow(paint_type); 354} 355 356gfx::Insets TrayBubbleView::GetBorderInsets() const { 357 return bubble_border_->GetInsets(); 358} 359 360void TrayBubbleView::Init() { 361 BoxLayout* layout = new BottomAlignedBoxLayout(this); 362 layout->set_spread_blank_space(true); 363 SetLayoutManager(layout); 364} 365 366gfx::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 374bool TrayBubbleView::CanActivate() const { 375 return params_.can_activate; 376} 377 378NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) { 379 BubbleFrameView* frame = new BubbleFrameView(margins()); 380 frame->SetBubbleBorder(bubble_border_); 381 return frame; 382} 383 384bool TrayBubbleView::WidgetHasHitTestMask() const { 385 return true; 386} 387 388void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const { 389 DCHECK(mask); 390 mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds())); 391} 392 393gfx::Size TrayBubbleView::GetPreferredSize() { 394 return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_)); 395} 396 397gfx::Size TrayBubbleView::GetMaximumSize() { 398 gfx::Size size = GetPreferredSize(); 399 size.set_width(params_.max_width); 400 return size; 401} 402 403int 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 416void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) { 417 if (delegate_) 418 delegate_->OnMouseEnteredView(); 419} 420 421void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) { 422 if (delegate_) 423 delegate_->OnMouseExitedView(); 424} 425 426void 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 433void TrayBubbleView::ChildPreferredSizeChanged(View* child) { 434 SizeToContents(); 435} 436 437void 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