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