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