status_bubble_views.cc revision dc0f95d653279beabeb9817299e2902918ba123e
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/browser_theme_provider.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 34using views::Widget; 35 36// The alpha and color of the bubble's shadow. 37static const SkColor kShadowColor = SkColorSetARGB(30, 0, 0, 0); 38 39// The roundedness of the edges of our bubble. 40static const int kBubbleCornerRadius = 4; 41 42// How close the mouse can get to the infobubble before it starts sliding 43// off-screen. 44static const int kMousePadding = 20; 45 46// The horizontal offset of the text within the status bubble, not including the 47// outer shadow ring. 48static 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. 52static const int kTextHorizPadding = 1; 53 54// Delays before we start hiding or showing the bubble after we receive a 55// show or hide request. 56static const int kShowDelay = 80; 57static const int kHideDelay = 250; 58 59// How long each fade should last for. 60static const int kShowFadeDurationMS = 120; 61static const int kHideFadeDurationMS = 200; 62static const int kFramerate = 25; 63 64// How long each expansion step should take. 65static const int kMinExpansionStepDurationMS = 20; 66static 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. 71class 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_provider_(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_provider_; 178}; 179 180void 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 195void StatusBubbleViews::StatusView::Show() { 196 Stop(); 197 CancelTimer(); 198 SetOpacity(1.0); 199 popup_->Show(); 200 stage_ = BUBBLE_SHOWN; 201 PaintNow(); 202} 203 204void StatusBubbleViews::StatusView::Hide() { 205 Stop(); 206 CancelTimer(); 207 SetOpacity(0.0); 208 text_.clear(); 209 popup_->Hide(); 210 stage_ = BUBBLE_HIDDEN; 211} 212 213void 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 222void 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 232void StatusBubbleViews::StatusView::CancelTimer() { 233 if (!timer_factory_.empty()) 234 timer_factory_.RevokeAll(); 235} 236 237void StatusBubbleViews::StatusView::RestartTimer(int delay) { 238 CancelTimer(); 239 StartTimer(delay); 240} 241 242void 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 250void 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 261void 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 279void 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. 305double StatusBubbleViews::StatusView::GetCurrentOpacity() { 306 return opacity_start_ + (opacity_end_ - opacity_start_) * 307 ui::LinearAnimation::GetCurrentValue(); 308} 309 310void StatusBubbleViews::StatusView::SetOpacity(double opacity) { 311 popup_->SetOpacity(static_cast<unsigned char>(opacity * 255)); 312 SchedulePaint(); 313} 314 315void StatusBubbleViews::StatusView::AnimateToState(double state) { 316 SetOpacity(GetCurrentOpacity()); 317} 318 319void 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 331void StatusBubbleViews::StatusView::SetStyle(BubbleStyle style) { 332 if (style_ != style) { 333 style_ = style; 334 SchedulePaint(); 335 } 336} 337 338void 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_provider_->GetColor(BrowserThemeProvider::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_provider_->GetColor(BrowserThemeProvider::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. 462class 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(string16 expanded_text, int current_width, 476 int expansion_end); 477 478 // Set width of fully expanded bubble. 479 void SetExpandedWidth(int expanded_width); 480 481 private: 482 // Animation functions. 483 int GetCurrentBubbleWidth(); 484 void SetBubbleWidth(int width); 485 void AnimateToState(double state); 486 void AnimationEnded(const ui::Animation* animation); 487 488 // Manager that owns us. 489 StatusBubbleViews* status_bubble_; 490 491 // Change the bounds and text of this view. 492 StatusView* status_view_; 493 494 // Text elided (if needed) to fit maximum status bar width. 495 string16 expanded_text_; 496 497 // Widths at expansion start and end. 498 int expansion_start_; 499 int expansion_end_; 500}; 501 502void StatusBubbleViews::StatusViewExpander::AnimateToState(double state) { 503 SetBubbleWidth(GetCurrentBubbleWidth()); 504} 505 506void StatusBubbleViews::StatusViewExpander::AnimationEnded( 507 const ui::Animation* animation) { 508 SetBubbleWidth(expansion_end_); 509 status_view_->SetText(expanded_text_, false); 510} 511 512void StatusBubbleViews::StatusViewExpander::StartExpansion( 513 string16 expanded_text, int expansion_start, 514 int expansion_end) { 515 expanded_text_ = expanded_text; 516 expansion_start_ = expansion_start; 517 expansion_end_ = expansion_end; 518 int min_duration = std::max(kMinExpansionStepDurationMS, 519 static_cast<int>(kMaxExpansionStepDurationMS * 520 (expansion_end - expansion_start) / 100.0)); 521 SetDuration(std::min(kMaxExpansionStepDurationMS, min_duration)); 522 Start(); 523} 524 525int StatusBubbleViews::StatusViewExpander::GetCurrentBubbleWidth() { 526 return static_cast<int>(expansion_start_ + 527 (expansion_end_ - expansion_start_) * 528 ui::LinearAnimation::GetCurrentValue()); 529} 530 531void StatusBubbleViews::StatusViewExpander::SetBubbleWidth(int width) { 532 status_bubble_->SetBubbleWidth(width); 533 status_view_->SchedulePaint(); 534} 535 536// StatusBubble --------------------------------------------------------------- 537 538const int StatusBubbleViews::kShadowThickness = 1; 539 540StatusBubbleViews::StatusBubbleViews(views::View* base_view) 541 : offset_(0), 542 popup_(NULL), 543 opacity_(0), 544 base_view_(base_view), 545 view_(NULL), 546 download_shelf_is_visible_(false), 547 is_expanded_(false), 548 ALLOW_THIS_IN_INITIALIZER_LIST(expand_timer_factory_(this)) { 549 expand_view_.reset(); 550} 551 552StatusBubbleViews::~StatusBubbleViews() { 553 CancelExpandTimer(); 554 if (popup_.get()) 555 popup_->CloseNow(); 556} 557 558void StatusBubbleViews::Init() { 559 if (!popup_.get()) { 560 popup_.reset(Widget::CreatePopupWidget(Widget::Transparent, 561 Widget::NotAcceptEvents, 562 Widget::NotDeleteOnDestroy, 563 Widget::MirrorOriginInRTL)); 564 views::Widget* frame = base_view_->GetWidget(); 565 if (!view_) 566 view_ = new StatusView(this, popup_.get(), frame->GetThemeProvider()); 567 if (!expand_view_.get()) 568 expand_view_.reset(new StatusViewExpander(this, view_)); 569 popup_->SetOpacity(0x00); 570 popup_->Init(frame->GetNativeView(), gfx::Rect()); 571 popup_->SetContentsView(view_); 572 Reposition(); 573 popup_->Show(); 574 } 575} 576 577void StatusBubbleViews::Reposition() { 578 if (popup_.get()) { 579 gfx::Point top_left; 580 views::View::ConvertPointToScreen(base_view_, &top_left); 581 582 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 583 top_left.y() + position_.y(), 584 size_.width(), size_.height())); 585 } 586} 587 588gfx::Size StatusBubbleViews::GetPreferredSize() { 589 return gfx::Size(0, ResourceBundle::GetSharedInstance().GetFont( 590 ResourceBundle::BaseFont).GetHeight() + kTotalVerticalPadding); 591} 592 593void StatusBubbleViews::SetBounds(int x, int y, int w, int h) { 594 original_position_.SetPoint(x, y); 595 position_.SetPoint(base_view_->GetMirroredXWithWidthInView(x, w), y); 596 size_.SetSize(w, h); 597 Reposition(); 598} 599 600void StatusBubbleViews::SetStatus(const string16& status_text) { 601 if (size_.IsEmpty()) 602 return; // We have no bounds, don't attempt to show the popup. 603 604 if (status_text_ == status_text && !status_text.empty()) 605 return; 606 607 if (!IsFrameVisible()) 608 return; // Don't show anything if the parent isn't visible. 609 610 Init(); 611 status_text_ = status_text; 612 if (!status_text_.empty()) { 613 view_->SetText(status_text, true); 614 view_->Show(); 615 } else if (!url_text_.empty()) { 616 view_->SetText(url_text_, true); 617 } else { 618 view_->SetText(string16(), true); 619 } 620} 621 622void StatusBubbleViews::SetURL(const GURL& url, const string16& languages) { 623 languages_ = languages; 624 url_ = url; 625 if (size_.IsEmpty()) 626 return; // We have no bounds, don't attempt to show the popup. 627 628 Init(); 629 630 // If we want to clear a displayed URL but there is a status still to 631 // display, display that status instead. 632 if (url.is_empty() && !status_text_.empty()) { 633 url_text_ = string16(); 634 if (IsFrameVisible()) 635 view_->SetText(status_text_, true); 636 return; 637 } 638 639 // Reset expansion state only when bubble is completely hidden. 640 if (view_->GetState() == StatusView::BUBBLE_HIDDEN) { 641 is_expanded_ = false; 642 SetBubbleWidth(GetStandardStatusBubbleWidth()); 643 } 644 645 // Set Elided Text corresponding to the GURL object. 646 gfx::Rect popup_bounds = popup_->GetWindowScreenBounds(); 647 int text_width = static_cast<int>(popup_bounds.width() - 648 (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1); 649 url_text_ = ui::ElideUrl(url, view_->Label::font(), 650 text_width, UTF16ToUTF8(languages)); 651 652 std::wstring original_url_text = 653 UTF16ToWideHack(net::FormatUrl(url, UTF16ToUTF8(languages))); 654 655 // An URL is always treated as a left-to-right string. On right-to-left UIs 656 // we need to explicitly mark the URL as LTR to make sure it is displayed 657 // correctly. 658 url_text_ = base::i18n::GetDisplayStringInLTRDirectionality(url_text_); 659 660 if (IsFrameVisible()) { 661 view_->SetText(url_text_, true); 662 663 CancelExpandTimer(); 664 665 // If bubble is already in expanded state, shift to adjust to new text 666 // size (shrinking or expanding). Otherwise delay. 667 if (is_expanded_ && !url.is_empty()) 668 ExpandBubble(); 669 else if (original_url_text.length() > url_text_.length()) 670 MessageLoop::current()->PostDelayedTask(FROM_HERE, 671 expand_timer_factory_.NewRunnableMethod( 672 &StatusBubbleViews::ExpandBubble), kExpandHoverDelay); 673 } 674} 675 676void StatusBubbleViews::Hide() { 677 status_text_ = string16(); 678 url_text_ = string16(); 679 if (view_) 680 view_->Hide(); 681} 682 683void StatusBubbleViews::MouseMoved(const gfx::Point& location, 684 bool left_content) { 685 if (left_content) 686 return; 687 688 if (view_) { 689 view_->ResetTimer(); 690 691 if (view_->GetState() != StatusView::BUBBLE_HIDDEN && 692 view_->GetState() != StatusView::BUBBLE_HIDING_FADE && 693 view_->GetState() != StatusView::BUBBLE_HIDING_TIMER) { 694 AvoidMouse(location); 695 } 696 } 697} 698 699void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) { 700 download_shelf_is_visible_ = visible; 701} 702 703void StatusBubbleViews::AvoidMouse(const gfx::Point& location) { 704 // Get the position of the frame. 705 gfx::Point top_left; 706 views::View::ConvertPointToScreen(base_view_, &top_left); 707 // Border included. 708 int window_width = base_view_->GetLocalBounds().width(); 709 710 // Get the cursor position relative to the popup. 711 gfx::Point relative_location = location; 712 if (base::i18n::IsRTL()) { 713 int top_right_x = top_left.x() + window_width; 714 relative_location.set_x(top_right_x - relative_location.x()); 715 } else { 716 relative_location.set_x( 717 relative_location.x() - (top_left.x() + position_.x())); 718 } 719 relative_location.set_y( 720 relative_location.y() - (top_left.y() + position_.y())); 721 722 // If the mouse is in a position where we think it would move the 723 // status bubble, figure out where and how the bubble should be moved. 724 if (relative_location.y() > -kMousePadding && 725 relative_location.x() < size_.width() + kMousePadding) { 726 int offset = kMousePadding + relative_location.y(); 727 728 // Make the movement non-linear. 729 offset = offset * offset / kMousePadding; 730 731 // When the mouse is entering from the right, we want the offset to be 732 // scaled by how horizontally far away the cursor is from the bubble. 733 if (relative_location.x() > size_.width()) { 734 offset = static_cast<int>(static_cast<float>(offset) * ( 735 static_cast<float>(kMousePadding - 736 (relative_location.x() - size_.width())) / 737 static_cast<float>(kMousePadding))); 738 } 739 740 // Cap the offset and change the visual presentation of the bubble 741 // depending on where it ends up (so that rounded corners square off 742 // and mate to the edges of the tab content). 743 if (offset >= size_.height() - kShadowThickness * 2) { 744 offset = size_.height() - kShadowThickness * 2; 745 view_->SetStyle(StatusView::STYLE_BOTTOM); 746 } else if (offset > kBubbleCornerRadius / 2 - kShadowThickness) { 747 view_->SetStyle(StatusView::STYLE_FLOATING); 748 } else { 749 view_->SetStyle(StatusView::STYLE_STANDARD); 750 } 751 752 // Check if the bubble sticks out from the monitor or will obscure 753 // download shelf. 754 gfx::NativeView widget = base_view_->GetWidget()->GetNativeView(); 755 gfx::Rect monitor_rect = 756 views::Screen::GetMonitorWorkAreaNearestWindow(widget); 757 const int bubble_bottom_y = top_left.y() + position_.y() + size_.height(); 758 759 if (bubble_bottom_y + offset > monitor_rect.height() || 760 (download_shelf_is_visible_ && 761 (view_->GetStyle() == StatusView::STYLE_FLOATING || 762 view_->GetStyle() == StatusView::STYLE_BOTTOM))) { 763 // The offset is still too large. Move the bubble to the right and reset 764 // Y offset_ to zero. 765 view_->SetStyle(StatusView::STYLE_STANDARD_RIGHT); 766 offset_ = 0; 767 768 // Subtract border width + bubble width. 769 int right_position_x = window_width - (position_.x() + size_.width()); 770 popup_->SetBounds(gfx::Rect(top_left.x() + right_position_x, 771 top_left.y() + position_.y(), 772 size_.width(), size_.height())); 773 } else { 774 offset_ = offset; 775 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 776 top_left.y() + position_.y() + offset_, 777 size_.width(), size_.height())); 778 } 779 } else if (offset_ != 0 || 780 view_->GetStyle() == StatusView::STYLE_STANDARD_RIGHT) { 781 offset_ = 0; 782 view_->SetStyle(StatusView::STYLE_STANDARD); 783 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 784 top_left.y() + position_.y(), 785 size_.width(), size_.height())); 786 } 787} 788 789bool StatusBubbleViews::IsFrameVisible() { 790 views::Widget* frame = base_view_->GetWidget(); 791 if (!frame->IsVisible()) 792 return false; 793 794 views::Window* window = frame->GetWindow(); 795 return !window || !window->IsMinimized(); 796} 797 798void StatusBubbleViews::ExpandBubble() { 799 // Elide URL to maximum possible size, then check actual length (it may 800 // still be too long to fit) before expanding bubble. 801 gfx::Rect popup_bounds = popup_->GetWindowScreenBounds(); 802 int max_status_bubble_width = GetMaxStatusBubbleWidth(); 803 url_text_ = ui::ElideUrl(url_, view_->Label::font(), 804 max_status_bubble_width, UTF16ToUTF8(languages_)); 805 int expanded_bubble_width =std::max(GetStandardStatusBubbleWidth(), 806 std::min(view_->Label::font().GetStringWidth(url_text_) + 807 (kShadowThickness * 2) + kTextPositionX + 808 kTextHorizPadding + 1, 809 max_status_bubble_width)); 810 is_expanded_ = true; 811 expand_view_->StartExpansion(url_text_, popup_bounds.width(), 812 expanded_bubble_width); 813} 814 815int StatusBubbleViews::GetStandardStatusBubbleWidth() { 816 return base_view_->bounds().width() / 3; 817} 818 819int StatusBubbleViews::GetMaxStatusBubbleWidth() { 820 return static_cast<int>(std::max(0, base_view_->bounds().width() - 821 (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1 - 822 views::NativeScrollBar::GetVerticalScrollBarWidth())); 823} 824 825void StatusBubbleViews::SetBubbleWidth(int width) { 826 size_.set_width(width); 827 SetBounds(original_position_.x(), original_position_.y(), 828 size_.width(), size_.height()); 829} 830 831void StatusBubbleViews::CancelExpandTimer() { 832 if (!expand_timer_factory_.empty()) 833 expand_timer_factory_.RevokeAll(); 834} 835