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