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