status_bubble_views.cc revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
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 "chrome/browser/ui/views/status_bubble_views.h" 6 7#include <algorithm> 8 9#include "base/bind.h" 10#include "base/i18n/rtl.h" 11#include "base/message_loop/message_loop.h" 12#include "base/strings/string_util.h" 13#include "base/strings/utf_string_conversions.h" 14#include "chrome/browser/themes/theme_properties.h" 15#include "chrome/browser/ui/elide_url.h" 16#include "net/base/net_util.h" 17#include "third_party/skia/include/core/SkPaint.h" 18#include "third_party/skia/include/core/SkPath.h" 19#include "third_party/skia/include/core/SkRect.h" 20#include "ui/aura/window.h" 21#include "ui/base/theme_provider.h" 22#include "ui/gfx/animation/animation_delegate.h" 23#include "ui/gfx/animation/linear_animation.h" 24#include "ui/gfx/canvas.h" 25#include "ui/gfx/font_list.h" 26#include "ui/gfx/point.h" 27#include "ui/gfx/rect.h" 28#include "ui/gfx/screen.h" 29#include "ui/gfx/skia_util.h" 30#include "ui/gfx/text_elider.h" 31#include "ui/gfx/text_utils.h" 32#include "ui/native_theme/native_theme.h" 33#include "ui/views/controls/scrollbar/native_scroll_bar.h" 34#include "ui/views/widget/root_view.h" 35#include "ui/views/widget/widget.h" 36#include "url/gurl.h" 37 38#if defined(USE_ASH) 39#include "ash/wm/window_state.h" 40#endif 41 42// The alpha and color of the bubble's shadow. 43static const SkColor kShadowColor = SkColorSetARGB(30, 0, 0, 0); 44 45// The roundedness of the edges of our bubble. 46static const int kBubbleCornerRadius = 4; 47 48// How close the mouse can get to the infobubble before it starts sliding 49// off-screen. 50static const int kMousePadding = 20; 51 52// The horizontal offset of the text within the status bubble, not including the 53// outer shadow ring. 54static const int kTextPositionX = 3; 55 56// The minimum horizontal space between the (right) end of the text and the edge 57// of the status bubble, not including the outer shadow ring. 58static const int kTextHorizPadding = 1; 59 60// Delays before we start hiding or showing the bubble after we receive a 61// show or hide request. 62static const int kShowDelay = 80; 63static const int kHideDelay = 250; 64 65// How long each fade should last for. 66static const int kShowFadeDurationMS = 120; 67static const int kHideFadeDurationMS = 200; 68static const int kFramerate = 25; 69 70// How long each expansion step should take. 71static const int kMinExpansionStepDurationMS = 20; 72static const int kMaxExpansionStepDurationMS = 150; 73 74 75// StatusBubbleViews::StatusViewAnimation -------------------------------------- 76class StatusBubbleViews::StatusViewAnimation : public gfx::LinearAnimation, 77 public gfx::AnimationDelegate { 78 public: 79 StatusViewAnimation(StatusView* status_view, 80 double opacity_start, 81 double opacity_end); 82 virtual ~StatusViewAnimation(); 83 84 double GetCurrentOpacity(); 85 86 private: 87 // gfx::LinearAnimation: 88 virtual void AnimateToState(double state) OVERRIDE; 89 90 // gfx::AnimationDelegate: 91 virtual void AnimationEnded(const Animation* animation) OVERRIDE; 92 93 StatusView* status_view_; 94 95 // Start and end opacities for the current transition - note that as a 96 // fade-in can easily turn into a fade out, opacity_start_ is sometimes 97 // a value between 0 and 1. 98 double opacity_start_; 99 double opacity_end_; 100 101 DISALLOW_COPY_AND_ASSIGN(StatusViewAnimation); 102}; 103 104 105// StatusBubbleViews::StatusView ----------------------------------------------- 106// 107// StatusView manages the display of the bubble, applying text changes and 108// fading in or out the bubble as required. 109class StatusBubbleViews::StatusView : public views::View { 110 public: 111 // The bubble can be in one of many states: 112 enum BubbleState { 113 BUBBLE_HIDDEN, // Entirely BUBBLE_HIDDEN. 114 BUBBLE_HIDING_FADE, // In a fade-out transition. 115 BUBBLE_HIDING_TIMER, // Waiting before a fade-out. 116 BUBBLE_SHOWING_TIMER, // Waiting before a fade-in. 117 BUBBLE_SHOWING_FADE, // In a fade-in transition. 118 BUBBLE_SHOWN // Fully visible. 119 }; 120 121 enum BubbleStyle { 122 STYLE_BOTTOM, 123 STYLE_FLOATING, 124 STYLE_STANDARD, 125 STYLE_STANDARD_RIGHT 126 }; 127 128 StatusView(views::Widget* popup, 129 ui::ThemeProvider* theme_provider); 130 virtual ~StatusView(); 131 132 // Set the bubble text to a certain value, hides the bubble if text is 133 // an empty string. Trigger animation sequence to display if 134 // |should_animate_open|. 135 void SetText(const base::string16& text, bool should_animate_open); 136 137 BubbleState state() const { return state_; } 138 BubbleStyle style() const { return style_; } 139 void SetStyle(BubbleStyle style); 140 141 // Show the bubble instantly. 142 void Show(); 143 144 // Hide the bubble instantly. 145 void Hide(); 146 147 // Resets any timers we have. Typically called when the user moves a 148 // mouse. 149 void ResetTimer(); 150 151 // This call backs the StatusView in order to fade the bubble in and out. 152 void SetOpacity(double opacity); 153 154 // Depending on the state of the bubble this will either hide the popup or 155 // not. 156 void OnAnimationEnded(); 157 158 private: 159 class InitialTimer; 160 161 // Manage the timers that control the delay before a fade begins or ends. 162 void StartTimer(base::TimeDelta time); 163 void OnTimer(); 164 void CancelTimer(); 165 void RestartTimer(base::TimeDelta delay); 166 167 // Manage the fades and starting and stopping the animations correctly. 168 void StartFade(double start, double end, int duration); 169 void StartHiding(); 170 void StartShowing(); 171 172 // views::View: 173 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; 174 175 BubbleState state_; 176 BubbleStyle style_; 177 178 base::WeakPtrFactory<StatusBubbleViews::StatusView> timer_factory_; 179 180 scoped_ptr<StatusViewAnimation> animation_; 181 182 // Handle to the widget that contains us. 183 views::Widget* popup_; 184 185 // The currently-displayed text. 186 base::string16 text_; 187 188 // Holds the theme provider of the frame that created us. 189 ui::ThemeProvider* theme_service_; 190 191 DISALLOW_COPY_AND_ASSIGN(StatusView); 192}; 193 194StatusBubbleViews::StatusView::StatusView(views::Widget* popup, 195 ui::ThemeProvider* theme_provider) 196 : state_(BUBBLE_HIDDEN), 197 style_(STYLE_STANDARD), 198 timer_factory_(this), 199 animation_(new StatusViewAnimation(this, 0, 0)), 200 popup_(popup), 201 theme_service_(theme_provider) { 202} 203 204StatusBubbleViews::StatusView::~StatusView() { 205 animation_->Stop(); 206 CancelTimer(); 207} 208 209void StatusBubbleViews::StatusView::SetText(const base::string16& text, 210 bool should_animate_open) { 211 if (text.empty()) { 212 // The string was empty. 213 StartHiding(); 214 } else { 215 // We want to show the string. 216 if (text != text_) { 217 text_ = text; 218 SchedulePaint(); 219 } 220 if (should_animate_open) 221 StartShowing(); 222 } 223} 224 225void StatusBubbleViews::StatusView::Show() { 226 animation_->Stop(); 227 CancelTimer(); 228 SetOpacity(1.0); 229 popup_->ShowInactive(); 230 state_ = BUBBLE_SHOWN; 231} 232 233void StatusBubbleViews::StatusView::Hide() { 234 animation_->Stop(); 235 CancelTimer(); 236 SetOpacity(0.0); 237 text_.clear(); 238 popup_->Hide(); 239 state_ = BUBBLE_HIDDEN; 240} 241 242void StatusBubbleViews::StatusView::StartTimer(base::TimeDelta time) { 243 if (timer_factory_.HasWeakPtrs()) 244 timer_factory_.InvalidateWeakPtrs(); 245 246 base::MessageLoop::current()->PostDelayedTask( 247 FROM_HERE, 248 base::Bind(&StatusBubbleViews::StatusView::OnTimer, 249 timer_factory_.GetWeakPtr()), 250 time); 251} 252 253void StatusBubbleViews::StatusView::OnTimer() { 254 if (state_ == BUBBLE_HIDING_TIMER) { 255 state_ = BUBBLE_HIDING_FADE; 256 StartFade(1.0, 0.0, kHideFadeDurationMS); 257 } else if (state_ == BUBBLE_SHOWING_TIMER) { 258 state_ = BUBBLE_SHOWING_FADE; 259 StartFade(0.0, 1.0, kShowFadeDurationMS); 260 } 261} 262 263void StatusBubbleViews::StatusView::CancelTimer() { 264 if (timer_factory_.HasWeakPtrs()) 265 timer_factory_.InvalidateWeakPtrs(); 266} 267 268void StatusBubbleViews::StatusView::RestartTimer(base::TimeDelta delay) { 269 CancelTimer(); 270 StartTimer(delay); 271} 272 273void StatusBubbleViews::StatusView::ResetTimer() { 274 if (state_ == BUBBLE_SHOWING_TIMER) { 275 // We hadn't yet begun showing anything when we received a new request 276 // for something to show, so we start from scratch. 277 RestartTimer(base::TimeDelta::FromMilliseconds(kShowDelay)); 278 } 279} 280 281void StatusBubbleViews::StatusView::StartFade(double start, 282 double end, 283 int duration) { 284 animation_.reset(new StatusViewAnimation(this, start, end)); 285 286 // This will also reset the currently-occurring animation. 287 animation_->SetDuration(duration); 288 animation_->Start(); 289} 290 291void StatusBubbleViews::StatusView::StartHiding() { 292 if (state_ == BUBBLE_SHOWN) { 293 state_ = BUBBLE_HIDING_TIMER; 294 StartTimer(base::TimeDelta::FromMilliseconds(kHideDelay)); 295 } else if (state_ == BUBBLE_SHOWING_TIMER) { 296 state_ = BUBBLE_HIDDEN; 297 popup_->Hide(); 298 CancelTimer(); 299 } else if (state_ == BUBBLE_SHOWING_FADE) { 300 state_ = BUBBLE_HIDING_FADE; 301 // Figure out where we are in the current fade. 302 double current_opacity = animation_->GetCurrentOpacity(); 303 304 // Start a fade in the opposite direction. 305 StartFade(current_opacity, 0.0, 306 static_cast<int>(kHideFadeDurationMS * current_opacity)); 307 } 308} 309 310void StatusBubbleViews::StatusView::StartShowing() { 311 if (state_ == BUBBLE_HIDDEN) { 312 popup_->ShowInactive(); 313 state_ = BUBBLE_SHOWING_TIMER; 314 StartTimer(base::TimeDelta::FromMilliseconds(kShowDelay)); 315 } else if (state_ == BUBBLE_HIDING_TIMER) { 316 state_ = BUBBLE_SHOWN; 317 CancelTimer(); 318 } else if (state_ == BUBBLE_HIDING_FADE) { 319 // We're partway through a fade. 320 state_ = BUBBLE_SHOWING_FADE; 321 322 // Figure out where we are in the current fade. 323 double current_opacity = animation_->GetCurrentOpacity(); 324 325 // Start a fade in the opposite direction. 326 StartFade(current_opacity, 1.0, 327 static_cast<int>(kShowFadeDurationMS * current_opacity)); 328 } else if (state_ == BUBBLE_SHOWING_TIMER) { 329 // We hadn't yet begun showing anything when we received a new request 330 // for something to show, so we start from scratch. 331 ResetTimer(); 332 } 333} 334 335void StatusBubbleViews::StatusView::SetOpacity(double opacity) { 336 popup_->SetOpacity(static_cast<unsigned char>(opacity * 255)); 337} 338 339void StatusBubbleViews::StatusView::SetStyle(BubbleStyle style) { 340 if (style_ != style) { 341 style_ = style; 342 SchedulePaint(); 343 } 344} 345 346void StatusBubbleViews::StatusView::OnAnimationEnded() { 347 if (state_ == BUBBLE_HIDING_FADE) { 348 state_ = BUBBLE_HIDDEN; 349 popup_->Hide(); 350 } else if (state_ == BUBBLE_SHOWING_FADE) { 351 state_ = BUBBLE_SHOWN; 352 } 353} 354 355void StatusBubbleViews::StatusView::OnPaint(gfx::Canvas* canvas) { 356 SkPaint paint; 357 paint.setStyle(SkPaint::kFill_Style); 358 paint.setAntiAlias(true); 359 SkColor toolbar_color = theme_service_->GetColor( 360 ThemeProperties::COLOR_TOOLBAR); 361 paint.setColor(toolbar_color); 362 363 gfx::Rect popup_bounds = popup_->GetWindowBoundsInScreen(); 364 365 // Figure out how to round the bubble's four corners. 366 SkScalar rad[8]; 367 368 // Top Edges - if the bubble is in its bottom position (sticking downwards), 369 // then we square the top edges. Otherwise, we square the edges based on the 370 // position of the bubble within the window (the bubble is positioned in the 371 // southeast corner in RTL and in the southwest corner in LTR). 372 if (style_ == STYLE_BOTTOM) { 373 // Top Left corner. 374 rad[0] = 0; 375 rad[1] = 0; 376 377 // Top Right corner. 378 rad[2] = 0; 379 rad[3] = 0; 380 } else { 381 if (base::i18n::IsRTL() != (style_ == STYLE_STANDARD_RIGHT)) { 382 // The text is RtL or the bubble is on the right side (but not both). 383 384 // Top Left corner. 385 rad[0] = SkIntToScalar(kBubbleCornerRadius); 386 rad[1] = SkIntToScalar(kBubbleCornerRadius); 387 388 // Top Right corner. 389 rad[2] = 0; 390 rad[3] = 0; 391 } else { 392 // Top Left corner. 393 rad[0] = 0; 394 rad[1] = 0; 395 396 // Top Right corner. 397 rad[2] = SkIntToScalar(kBubbleCornerRadius); 398 rad[3] = SkIntToScalar(kBubbleCornerRadius); 399 } 400 } 401 402 // Bottom edges - square these off if the bubble is in its standard position 403 // (sticking upward). 404 if (style_ == STYLE_STANDARD || style_ == STYLE_STANDARD_RIGHT) { 405 // Bottom Right Corner. 406 rad[4] = 0; 407 rad[5] = 0; 408 409 // Bottom Left Corner. 410 rad[6] = 0; 411 rad[7] = 0; 412 } else { 413 // Bottom Right Corner. 414 rad[4] = SkIntToScalar(kBubbleCornerRadius); 415 rad[5] = SkIntToScalar(kBubbleCornerRadius); 416 417 // Bottom Left Corner. 418 rad[6] = SkIntToScalar(kBubbleCornerRadius); 419 rad[7] = SkIntToScalar(kBubbleCornerRadius); 420 } 421 422 // Draw the bubble's shadow. 423 int width = popup_bounds.width(); 424 int height = popup_bounds.height(); 425 SkRect rect(gfx::RectToSkRect(gfx::Rect(popup_bounds.size()))); 426 SkPath shadow_path; 427 shadow_path.addRoundRect(rect, rad, SkPath::kCW_Direction); 428 SkPaint shadow_paint; 429 shadow_paint.setAntiAlias(true); 430 shadow_paint.setColor(kShadowColor); 431 canvas->DrawPath(shadow_path, shadow_paint); 432 433 // Draw the bubble. 434 rect.set(SkIntToScalar(kShadowThickness), 435 SkIntToScalar(kShadowThickness), 436 SkIntToScalar(width - kShadowThickness), 437 SkIntToScalar(height - kShadowThickness)); 438 SkPath path; 439 path.addRoundRect(rect, rad, SkPath::kCW_Direction); 440 canvas->DrawPath(path, paint); 441 442 // Draw highlight text and then the text body. In order to make sure the text 443 // is aligned to the right on RTL UIs, we mirror the text bounds if the 444 // locale is RTL. 445 const gfx::FontList font_list; 446 int text_width = std::min( 447 gfx::GetStringWidth(text_, font_list), 448 width - (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding); 449 int text_height = height - (kShadowThickness * 2); 450 gfx::Rect body_bounds(kShadowThickness + kTextPositionX, 451 kShadowThickness, 452 std::max(0, text_width), 453 std::max(0, text_height)); 454 body_bounds.set_x(GetMirroredXForRect(body_bounds)); 455 SkColor text_color = 456 theme_service_->GetColor(ThemeProperties::COLOR_STATUS_BAR_TEXT); 457 canvas->DrawStringRect(text_, font_list, text_color, body_bounds); 458} 459 460 461// StatusBubbleViews::StatusViewAnimation -------------------------------------- 462 463StatusBubbleViews::StatusViewAnimation::StatusViewAnimation( 464 StatusView* status_view, 465 double opacity_start, 466 double opacity_end) 467 : gfx::LinearAnimation(kFramerate, this), 468 status_view_(status_view), 469 opacity_start_(opacity_start), 470 opacity_end_(opacity_end) { 471} 472 473StatusBubbleViews::StatusViewAnimation::~StatusViewAnimation() { 474 // Remove ourself as a delegate so that we don't get notified when 475 // animations end as a result of destruction. 476 set_delegate(NULL); 477} 478 479double StatusBubbleViews::StatusViewAnimation::GetCurrentOpacity() { 480 return opacity_start_ + (opacity_end_ - opacity_start_) * 481 gfx::LinearAnimation::GetCurrentValue(); 482} 483 484void StatusBubbleViews::StatusViewAnimation::AnimateToState(double state) { 485 status_view_->SetOpacity(GetCurrentOpacity()); 486} 487 488void StatusBubbleViews::StatusViewAnimation::AnimationEnded( 489 const gfx::Animation* animation) { 490 status_view_->SetOpacity(opacity_end_); 491 status_view_->OnAnimationEnded(); 492} 493 494// StatusBubbleViews::StatusViewExpander --------------------------------------- 495// 496// Manages the expansion and contraction of the status bubble as it accommodates 497// URLs too long to fit in the standard bubble. Changes are passed through the 498// StatusView to paint. 499class StatusBubbleViews::StatusViewExpander : public gfx::LinearAnimation, 500 public gfx::AnimationDelegate { 501 public: 502 StatusViewExpander(StatusBubbleViews* status_bubble, 503 StatusView* status_view) 504 : gfx::LinearAnimation(kFramerate, this), 505 status_bubble_(status_bubble), 506 status_view_(status_view), 507 expansion_start_(0), 508 expansion_end_(0) { 509 } 510 511 // Manage the expansion of the bubble. 512 void StartExpansion(const base::string16& expanded_text, 513 int current_width, 514 int expansion_end); 515 516 // Set width of fully expanded bubble. 517 void SetExpandedWidth(int expanded_width); 518 519 private: 520 // Animation functions. 521 int GetCurrentBubbleWidth(); 522 void SetBubbleWidth(int width); 523 virtual void AnimateToState(double state) OVERRIDE; 524 virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE; 525 526 // Manager that owns us. 527 StatusBubbleViews* status_bubble_; 528 529 // Change the bounds and text of this view. 530 StatusView* status_view_; 531 532 // Text elided (if needed) to fit maximum status bar width. 533 base::string16 expanded_text_; 534 535 // Widths at expansion start and end. 536 int expansion_start_; 537 int expansion_end_; 538}; 539 540void StatusBubbleViews::StatusViewExpander::AnimateToState(double state) { 541 SetBubbleWidth(GetCurrentBubbleWidth()); 542} 543 544void StatusBubbleViews::StatusViewExpander::AnimationEnded( 545 const gfx::Animation* animation) { 546 SetBubbleWidth(expansion_end_); 547 status_view_->SetText(expanded_text_, false); 548} 549 550void StatusBubbleViews::StatusViewExpander::StartExpansion( 551 const base::string16& expanded_text, 552 int expansion_start, 553 int expansion_end) { 554 expanded_text_ = expanded_text; 555 expansion_start_ = expansion_start; 556 expansion_end_ = expansion_end; 557 int min_duration = std::max(kMinExpansionStepDurationMS, 558 static_cast<int>(kMaxExpansionStepDurationMS * 559 (expansion_end - expansion_start) / 100.0)); 560 SetDuration(std::min(kMaxExpansionStepDurationMS, min_duration)); 561 Start(); 562} 563 564int StatusBubbleViews::StatusViewExpander::GetCurrentBubbleWidth() { 565 return static_cast<int>(expansion_start_ + 566 (expansion_end_ - expansion_start_) * 567 gfx::LinearAnimation::GetCurrentValue()); 568} 569 570void StatusBubbleViews::StatusViewExpander::SetBubbleWidth(int width) { 571 status_bubble_->SetBubbleWidth(width); 572 status_view_->SchedulePaint(); 573} 574 575 576// StatusBubbleViews ----------------------------------------------------------- 577 578const int StatusBubbleViews::kShadowThickness = 1; 579 580StatusBubbleViews::StatusBubbleViews(views::View* base_view) 581 : contains_mouse_(false), 582 offset_(0), 583 base_view_(base_view), 584 view_(NULL), 585 download_shelf_is_visible_(false), 586 is_expanded_(false), 587 expand_timer_factory_(this) { 588 expand_view_.reset(); 589} 590 591StatusBubbleViews::~StatusBubbleViews() { 592 CancelExpandTimer(); 593 if (popup_.get()) 594 popup_->CloseNow(); 595} 596 597void StatusBubbleViews::Init() { 598 if (!popup_.get()) { 599 popup_.reset(new views::Widget); 600 views::Widget* frame = base_view_->GetWidget(); 601 if (!view_) 602 view_ = new StatusView(popup_.get(), frame->GetThemeProvider()); 603 if (!expand_view_.get()) 604 expand_view_.reset(new StatusViewExpander(this, view_)); 605 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); 606 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 607 params.accept_events = false; 608 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 609 params.parent = frame->GetNativeView(); 610 params.context = frame->GetNativeView(); 611 popup_->Init(params); 612 popup_->GetNativeView()->SetName("StatusBubbleViews"); 613 // We do our own animation and don't want any from the system. 614 popup_->SetVisibilityChangedAnimationsEnabled(false); 615 popup_->SetOpacity(0x00); 616 popup_->SetContentsView(view_); 617#if defined(USE_ASH) 618 ash::wm::GetWindowState(popup_->GetNativeWindow())-> 619 set_ignored_by_shelf(true); 620#endif 621 RepositionPopup(); 622 } 623} 624 625void StatusBubbleViews::Reposition() { 626 // In restored mode, the client area has a client edge between it and the 627 // frame. 628 int overlap = kShadowThickness; 629 // The extra pixels defined by kClientEdgeThickness is only drawn in frame 630 // content border on windows for non-aura build. 631#if !defined(USE_ASH) 632 overlap += 633 IsFrameMaximized() ? 0 : views::NonClientFrameView::kClientEdgeThickness; 634#endif 635 int height = GetPreferredSize().height(); 636 int base_view_height = base_view()->bounds().height(); 637 gfx::Point origin(-overlap, base_view_height - height + overlap); 638 SetBounds(origin.x(), origin.y(), base_view()->bounds().width() / 3, height); 639} 640 641void StatusBubbleViews::RepositionPopup() { 642 if (popup_.get()) { 643 gfx::Point top_left; 644 views::View::ConvertPointToScreen(base_view_, &top_left); 645 646 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 647 top_left.y() + position_.y(), 648 size_.width(), size_.height())); 649 } 650} 651 652gfx::Size StatusBubbleViews::GetPreferredSize() { 653 return gfx::Size(0, gfx::FontList().GetHeight() + kTotalVerticalPadding); 654} 655 656void StatusBubbleViews::SetBounds(int x, int y, int w, int h) { 657 original_position_.SetPoint(x, y); 658 position_.SetPoint(base_view_->GetMirroredXWithWidthInView(x, w), y); 659 size_.SetSize(w, h); 660 RepositionPopup(); 661 if (popup_.get() && contains_mouse_) 662 AvoidMouse(last_mouse_moved_location_); 663} 664 665void StatusBubbleViews::SetStatus(const base::string16& status_text) { 666 if (size_.IsEmpty()) 667 return; // We have no bounds, don't attempt to show the popup. 668 669 if (status_text_ == status_text && !status_text.empty()) 670 return; 671 672 if (!IsFrameVisible()) 673 return; // Don't show anything if the parent isn't visible. 674 675 Init(); 676 status_text_ = status_text; 677 if (!status_text_.empty()) { 678 view_->SetText(status_text, true); 679 view_->Show(); 680 } else if (!url_text_.empty()) { 681 view_->SetText(url_text_, true); 682 } else { 683 view_->SetText(base::string16(), true); 684 } 685} 686 687void StatusBubbleViews::SetURL(const GURL& url, const std::string& languages) { 688 url_ = url; 689 languages_ = languages; 690 if (size_.IsEmpty()) 691 return; // We have no bounds, don't attempt to show the popup. 692 693 Init(); 694 695 // If we want to clear a displayed URL but there is a status still to 696 // display, display that status instead. 697 if (url.is_empty() && !status_text_.empty()) { 698 url_text_ = base::string16(); 699 if (IsFrameVisible()) 700 view_->SetText(status_text_, true); 701 return; 702 } 703 704 // Reset expansion state only when bubble is completely hidden. 705 if (view_->state() == StatusView::BUBBLE_HIDDEN) { 706 is_expanded_ = false; 707 SetBubbleWidth(GetStandardStatusBubbleWidth()); 708 } 709 710 // Set Elided Text corresponding to the GURL object. 711 gfx::Rect popup_bounds = popup_->GetWindowBoundsInScreen(); 712 int text_width = static_cast<int>(popup_bounds.width() - 713 (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1); 714 url_text_ = ElideUrl(url, gfx::FontList(), text_width, languages); 715 716 // An URL is always treated as a left-to-right string. On right-to-left UIs 717 // we need to explicitly mark the URL as LTR to make sure it is displayed 718 // correctly. 719 url_text_ = base::i18n::GetDisplayStringInLTRDirectionality(url_text_); 720 721 if (IsFrameVisible()) { 722 view_->SetText(url_text_, true); 723 724 CancelExpandTimer(); 725 726 // If bubble is already in expanded state, shift to adjust to new text 727 // size (shrinking or expanding). Otherwise delay. 728 if (is_expanded_ && !url.is_empty()) { 729 ExpandBubble(); 730 } else if (net::FormatUrl(url, languages).length() > url_text_.length()) { 731 base::MessageLoop::current()->PostDelayedTask( 732 FROM_HERE, 733 base::Bind(&StatusBubbleViews::ExpandBubble, 734 expand_timer_factory_.GetWeakPtr()), 735 base::TimeDelta::FromMilliseconds(kExpandHoverDelayMS)); 736 } 737 } 738} 739 740void StatusBubbleViews::Hide() { 741 status_text_ = base::string16(); 742 url_text_ = base::string16(); 743 if (view_) 744 view_->Hide(); 745} 746 747void StatusBubbleViews::MouseMoved(const gfx::Point& location, 748 bool left_content) { 749 contains_mouse_ = !left_content; 750 if (left_content) { 751 RepositionPopup(); 752 return; 753 } 754 last_mouse_moved_location_ = location; 755 756 if (view_) { 757 view_->ResetTimer(); 758 759 if (view_->state() != StatusView::BUBBLE_HIDDEN && 760 view_->state() != StatusView::BUBBLE_HIDING_FADE && 761 view_->state() != StatusView::BUBBLE_HIDING_TIMER) { 762 AvoidMouse(location); 763 } 764 } 765} 766 767void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) { 768 download_shelf_is_visible_ = visible; 769} 770 771void StatusBubbleViews::AvoidMouse(const gfx::Point& location) { 772 // Get the position of the frame. 773 gfx::Point top_left; 774 views::View::ConvertPointToScreen(base_view_, &top_left); 775 // Border included. 776 int window_width = base_view_->GetLocalBounds().width(); 777 778 // Get the cursor position relative to the popup. 779 gfx::Point relative_location = location; 780 if (base::i18n::IsRTL()) { 781 int top_right_x = top_left.x() + window_width; 782 relative_location.set_x(top_right_x - relative_location.x()); 783 } else { 784 relative_location.set_x( 785 relative_location.x() - (top_left.x() + position_.x())); 786 } 787 relative_location.set_y( 788 relative_location.y() - (top_left.y() + position_.y())); 789 790 // If the mouse is in a position where we think it would move the 791 // status bubble, figure out where and how the bubble should be moved. 792 if (relative_location.y() > -kMousePadding && 793 relative_location.x() < size_.width() + kMousePadding) { 794 int offset = kMousePadding + relative_location.y(); 795 796 // Make the movement non-linear. 797 offset = offset * offset / kMousePadding; 798 799 // When the mouse is entering from the right, we want the offset to be 800 // scaled by how horizontally far away the cursor is from the bubble. 801 if (relative_location.x() > size_.width()) { 802 offset = static_cast<int>(static_cast<float>(offset) * ( 803 static_cast<float>(kMousePadding - 804 (relative_location.x() - size_.width())) / 805 static_cast<float>(kMousePadding))); 806 } 807 808 // Cap the offset and change the visual presentation of the bubble 809 // depending on where it ends up (so that rounded corners square off 810 // and mate to the edges of the tab content). 811 if (offset >= size_.height() - kShadowThickness * 2) { 812 offset = size_.height() - kShadowThickness * 2; 813 view_->SetStyle(StatusView::STYLE_BOTTOM); 814 } else if (offset > kBubbleCornerRadius / 2 - kShadowThickness) { 815 view_->SetStyle(StatusView::STYLE_FLOATING); 816 } else { 817 view_->SetStyle(StatusView::STYLE_STANDARD); 818 } 819 820 // Check if the bubble sticks out from the monitor or will obscure 821 // download shelf. 822 gfx::NativeView window = base_view_->GetWidget()->GetNativeView(); 823 gfx::Rect monitor_rect = gfx::Screen::GetScreenFor(window)-> 824 GetDisplayNearestWindow(window).work_area(); 825 const int bubble_bottom_y = top_left.y() + position_.y() + size_.height(); 826 827 if (bubble_bottom_y + offset > monitor_rect.height() || 828 (download_shelf_is_visible_ && 829 (view_->style() == StatusView::STYLE_FLOATING || 830 view_->style() == StatusView::STYLE_BOTTOM))) { 831 // The offset is still too large. Move the bubble to the right and reset 832 // Y offset_ to zero. 833 view_->SetStyle(StatusView::STYLE_STANDARD_RIGHT); 834 offset_ = 0; 835 836 // Subtract border width + bubble width. 837 int right_position_x = window_width - (position_.x() + size_.width()); 838 popup_->SetBounds(gfx::Rect(top_left.x() + right_position_x, 839 top_left.y() + position_.y(), 840 size_.width(), size_.height())); 841 } else { 842 offset_ = offset; 843 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 844 top_left.y() + position_.y() + offset_, 845 size_.width(), size_.height())); 846 } 847 } else if (offset_ != 0 || 848 view_->style() == StatusView::STYLE_STANDARD_RIGHT) { 849 offset_ = 0; 850 view_->SetStyle(StatusView::STYLE_STANDARD); 851 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 852 top_left.y() + position_.y(), 853 size_.width(), size_.height())); 854 } 855} 856 857bool StatusBubbleViews::IsFrameVisible() { 858 views::Widget* frame = base_view_->GetWidget(); 859 if (!frame->IsVisible()) 860 return false; 861 862 views::Widget* window = frame->GetTopLevelWidget(); 863 return !window || !window->IsMinimized(); 864} 865 866bool StatusBubbleViews::IsFrameMaximized() { 867 views::Widget* frame = base_view_->GetWidget(); 868 views::Widget* window = frame->GetTopLevelWidget(); 869 return window && window->IsMaximized(); 870} 871 872void StatusBubbleViews::ExpandBubble() { 873 // Elide URL to maximum possible size, then check actual length (it may 874 // still be too long to fit) before expanding bubble. 875 gfx::Rect popup_bounds = popup_->GetWindowBoundsInScreen(); 876 int max_status_bubble_width = GetMaxStatusBubbleWidth(); 877 const gfx::FontList font_list; 878 url_text_ = ElideUrl(url_, font_list, max_status_bubble_width, languages_); 879 int expanded_bubble_width = 880 std::max(GetStandardStatusBubbleWidth(), 881 std::min(gfx::GetStringWidth(url_text_, font_list) + 882 (kShadowThickness * 2) + kTextPositionX + 883 kTextHorizPadding + 1, 884 max_status_bubble_width)); 885 is_expanded_ = true; 886 expand_view_->StartExpansion(url_text_, popup_bounds.width(), 887 expanded_bubble_width); 888} 889 890int StatusBubbleViews::GetStandardStatusBubbleWidth() { 891 return base_view_->bounds().width() / 3; 892} 893 894int StatusBubbleViews::GetMaxStatusBubbleWidth() { 895 const ui::NativeTheme* theme = base_view_->GetNativeTheme(); 896 return static_cast<int>(std::max(0, base_view_->bounds().width() - 897 (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1 - 898 views::NativeScrollBar::GetVerticalScrollBarWidth(theme))); 899} 900 901void StatusBubbleViews::SetBubbleWidth(int width) { 902 size_.set_width(width); 903 SetBounds(original_position_.x(), original_position_.y(), 904 size_.width(), size_.height()); 905} 906 907void StatusBubbleViews::CancelExpandTimer() { 908 if (expand_timer_factory_.HasWeakPtrs()) 909 expand_timer_factory_.InvalidateWeakPtrs(); 910} 911