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