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/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/base/theme_provider.h" 26#include "ui/gfx/canvas.h" 27#include "ui/gfx/point.h" 28#include "ui/gfx/screen.h" 29#include "ui/gfx/skia_util.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/property_util.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 ui::LinearAnimation, 78 public ui::AnimationDelegate { 79 public: 80 StatusView(StatusBubble* status_bubble, 81 views::Widget* popup, 82 ui::ThemeProvider* theme_provider) 83 : ui::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 ui::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 ui::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 ui::LinearAnimation, 469 public ui::AnimationDelegate { 470 public: 471 StatusViewExpander(StatusBubbleViews* status_bubble, 472 StatusView* status_view) 473 : ui::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 ui::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 ui::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 ui::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::SetIgnoredByShelf(popup_->GetNativeWindow(), true); 587#endif 588 Reposition(); 589 } 590} 591 592void StatusBubbleViews::Reposition() { 593 if (popup_.get()) { 594 gfx::Point top_left; 595 views::View::ConvertPointToScreen(base_view_, &top_left); 596 597 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 598 top_left.y() + position_.y(), 599 size_.width(), size_.height())); 600 } 601} 602 603gfx::Size StatusBubbleViews::GetPreferredSize() { 604 return gfx::Size(0, ui::ResourceBundle::GetSharedInstance().GetFont( 605 ui::ResourceBundle::BaseFont).GetHeight() + kTotalVerticalPadding); 606} 607 608void StatusBubbleViews::SetBounds(int x, int y, int w, int h) { 609 original_position_.SetPoint(x, y); 610 position_.SetPoint(base_view_->GetMirroredXWithWidthInView(x, w), y); 611 size_.SetSize(w, h); 612 Reposition(); 613 if (popup_.get() && contains_mouse_) 614 AvoidMouse(last_mouse_moved_location_); 615} 616 617void StatusBubbleViews::SetStatus(const string16& status_text) { 618 if (size_.IsEmpty()) 619 return; // We have no bounds, don't attempt to show the popup. 620 621 if (status_text_ == status_text && !status_text.empty()) 622 return; 623 624 if (!IsFrameVisible()) 625 return; // Don't show anything if the parent isn't visible. 626 627 Init(); 628 status_text_ = status_text; 629 if (!status_text_.empty()) { 630 view_->SetText(status_text, true); 631 view_->Show(); 632 } else if (!url_text_.empty()) { 633 view_->SetText(url_text_, true); 634 } else { 635 view_->SetText(string16(), true); 636 } 637} 638 639void StatusBubbleViews::SetURL(const GURL& url, const std::string& languages) { 640 url_ = url; 641 languages_ = languages; 642 if (size_.IsEmpty()) 643 return; // We have no bounds, don't attempt to show the popup. 644 645 Init(); 646 647 // If we want to clear a displayed URL but there is a status still to 648 // display, display that status instead. 649 if (url.is_empty() && !status_text_.empty()) { 650 url_text_ = string16(); 651 if (IsFrameVisible()) 652 view_->SetText(status_text_, true); 653 return; 654 } 655 656 // Reset expansion state only when bubble is completely hidden. 657 if (view_->GetState() == StatusView::BUBBLE_HIDDEN) { 658 is_expanded_ = false; 659 SetBubbleWidth(GetStandardStatusBubbleWidth()); 660 } 661 662 // Set Elided Text corresponding to the GURL object. 663 gfx::Rect popup_bounds = popup_->GetWindowBoundsInScreen(); 664 int text_width = static_cast<int>(popup_bounds.width() - 665 (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1); 666 url_text_ = ui::ElideUrl(url, view_->Label::font(), text_width, languages); 667 668 // An URL is always treated as a left-to-right string. On right-to-left UIs 669 // we need to explicitly mark the URL as LTR to make sure it is displayed 670 // correctly. 671 url_text_ = base::i18n::GetDisplayStringInLTRDirectionality(url_text_); 672 673 if (IsFrameVisible()) { 674 view_->SetText(url_text_, true); 675 676 CancelExpandTimer(); 677 678 // If bubble is already in expanded state, shift to adjust to new text 679 // size (shrinking or expanding). Otherwise delay. 680 if (is_expanded_ && !url.is_empty()) { 681 ExpandBubble(); 682 } else if (net::FormatUrl(url, languages).length() > url_text_.length()) { 683 base::MessageLoop::current()->PostDelayedTask( 684 FROM_HERE, 685 base::Bind(&StatusBubbleViews::ExpandBubble, 686 expand_timer_factory_.GetWeakPtr()), 687 base::TimeDelta::FromMilliseconds(kExpandHoverDelay)); 688 } 689 } 690} 691 692void StatusBubbleViews::Hide() { 693 status_text_ = string16(); 694 url_text_ = string16(); 695 if (view_) 696 view_->Hide(); 697} 698 699void StatusBubbleViews::MouseMoved(const gfx::Point& location, 700 bool left_content) { 701 contains_mouse_ = !left_content; 702 if (left_content) { 703 Reposition(); 704 return; 705 } 706 last_mouse_moved_location_ = location; 707 708 if (view_) { 709 view_->ResetTimer(); 710 711 if (view_->GetState() != StatusView::BUBBLE_HIDDEN && 712 view_->GetState() != StatusView::BUBBLE_HIDING_FADE && 713 view_->GetState() != StatusView::BUBBLE_HIDING_TIMER) { 714 AvoidMouse(location); 715 } 716 } 717} 718 719void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) { 720 download_shelf_is_visible_ = visible; 721} 722 723void StatusBubbleViews::AvoidMouse(const gfx::Point& location) { 724 // Get the position of the frame. 725 gfx::Point top_left; 726 views::View::ConvertPointToScreen(base_view_, &top_left); 727 // Border included. 728 int window_width = base_view_->GetLocalBounds().width(); 729 730 // Get the cursor position relative to the popup. 731 gfx::Point relative_location = location; 732 if (base::i18n::IsRTL()) { 733 int top_right_x = top_left.x() + window_width; 734 relative_location.set_x(top_right_x - relative_location.x()); 735 } else { 736 relative_location.set_x( 737 relative_location.x() - (top_left.x() + position_.x())); 738 } 739 relative_location.set_y( 740 relative_location.y() - (top_left.y() + position_.y())); 741 742 // If the mouse is in a position where we think it would move the 743 // status bubble, figure out where and how the bubble should be moved. 744 if (relative_location.y() > -kMousePadding && 745 relative_location.x() < size_.width() + kMousePadding) { 746 int offset = kMousePadding + relative_location.y(); 747 748 // Make the movement non-linear. 749 offset = offset * offset / kMousePadding; 750 751 // When the mouse is entering from the right, we want the offset to be 752 // scaled by how horizontally far away the cursor is from the bubble. 753 if (relative_location.x() > size_.width()) { 754 offset = static_cast<int>(static_cast<float>(offset) * ( 755 static_cast<float>(kMousePadding - 756 (relative_location.x() - size_.width())) / 757 static_cast<float>(kMousePadding))); 758 } 759 760 // Cap the offset and change the visual presentation of the bubble 761 // depending on where it ends up (so that rounded corners square off 762 // and mate to the edges of the tab content). 763 if (offset >= size_.height() - kShadowThickness * 2) { 764 offset = size_.height() - kShadowThickness * 2; 765 view_->SetStyle(StatusView::STYLE_BOTTOM); 766 } else if (offset > kBubbleCornerRadius / 2 - kShadowThickness) { 767 view_->SetStyle(StatusView::STYLE_FLOATING); 768 } else { 769 view_->SetStyle(StatusView::STYLE_STANDARD); 770 } 771 772 // Check if the bubble sticks out from the monitor or will obscure 773 // download shelf. 774 gfx::NativeView window = base_view_->GetWidget()->GetNativeView(); 775 gfx::Rect monitor_rect = gfx::Screen::GetScreenFor(window)-> 776 GetDisplayNearestWindow(window).work_area(); 777 const int bubble_bottom_y = top_left.y() + position_.y() + size_.height(); 778 779 if (bubble_bottom_y + offset > monitor_rect.height() || 780 (download_shelf_is_visible_ && 781 (view_->GetStyle() == StatusView::STYLE_FLOATING || 782 view_->GetStyle() == StatusView::STYLE_BOTTOM))) { 783 // The offset is still too large. Move the bubble to the right and reset 784 // Y offset_ to zero. 785 view_->SetStyle(StatusView::STYLE_STANDARD_RIGHT); 786 offset_ = 0; 787 788 // Subtract border width + bubble width. 789 int right_position_x = window_width - (position_.x() + size_.width()); 790 popup_->SetBounds(gfx::Rect(top_left.x() + right_position_x, 791 top_left.y() + position_.y(), 792 size_.width(), size_.height())); 793 } else { 794 offset_ = offset; 795 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 796 top_left.y() + position_.y() + offset_, 797 size_.width(), size_.height())); 798 } 799 } else if (offset_ != 0 || 800 view_->GetStyle() == StatusView::STYLE_STANDARD_RIGHT) { 801 offset_ = 0; 802 view_->SetStyle(StatusView::STYLE_STANDARD); 803 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 804 top_left.y() + position_.y(), 805 size_.width(), size_.height())); 806 } 807} 808 809bool StatusBubbleViews::IsFrameVisible() { 810 views::Widget* frame = base_view_->GetWidget(); 811 if (!frame->IsVisible()) 812 return false; 813 814 views::Widget* window = frame->GetTopLevelWidget(); 815 return !window || !window->IsMinimized(); 816} 817 818void StatusBubbleViews::ExpandBubble() { 819 // Elide URL to maximum possible size, then check actual length (it may 820 // still be too long to fit) before expanding bubble. 821 gfx::Rect popup_bounds = popup_->GetWindowBoundsInScreen(); 822 int max_status_bubble_width = GetMaxStatusBubbleWidth(); 823 url_text_ = ui::ElideUrl(url_, view_->Label::font(), 824 max_status_bubble_width, languages_); 825 int expanded_bubble_width =std::max(GetStandardStatusBubbleWidth(), 826 std::min(view_->Label::font().GetStringWidth(url_text_) + 827 (kShadowThickness * 2) + kTextPositionX + 828 kTextHorizPadding + 1, 829 max_status_bubble_width)); 830 is_expanded_ = true; 831 expand_view_->StartExpansion(url_text_, popup_bounds.width(), 832 expanded_bubble_width); 833} 834 835int StatusBubbleViews::GetStandardStatusBubbleWidth() { 836 return base_view_->bounds().width() / 3; 837} 838 839int StatusBubbleViews::GetMaxStatusBubbleWidth() { 840 const ui::NativeTheme* theme = base_view_->GetNativeTheme(); 841 return static_cast<int>(std::max(0, base_view_->bounds().width() - 842 (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1 - 843 views::NativeScrollBar::GetVerticalScrollBarWidth(theme))); 844} 845 846void StatusBubbleViews::SetBubbleWidth(int width) { 847 size_.set_width(width); 848 SetBounds(original_position_.x(), original_position_.y(), 849 size_.width(), size_.height()); 850} 851 852void StatusBubbleViews::CancelExpandTimer() { 853 if (expand_timer_factory_.HasWeakPtrs()) 854 expand_timer_factory_.InvalidateWeakPtrs(); 855} 856