status_bubble_views.cc revision f2477e01787aa58f445919b809d89e252beef54f
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_state.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 if (text != text_) { 196 text_ = text; 197 SchedulePaint(); 198 } 199 if (should_animate_open) 200 StartShowing(); 201 } 202} 203 204void StatusBubbleViews::StatusView::Show() { 205 Stop(); 206 CancelTimer(); 207 SetOpacity(1.0); 208 popup_->Show(); 209 stage_ = BUBBLE_SHOWN; 210} 211 212void StatusBubbleViews::StatusView::Hide() { 213 Stop(); 214 CancelTimer(); 215 SetOpacity(0.0); 216 text_.clear(); 217 popup_->Hide(); 218 stage_ = BUBBLE_HIDDEN; 219} 220 221void StatusBubbleViews::StatusView::StartTimer(base::TimeDelta time) { 222 if (timer_factory_.HasWeakPtrs()) 223 timer_factory_.InvalidateWeakPtrs(); 224 225 base::MessageLoop::current()->PostDelayedTask( 226 FROM_HERE, 227 base::Bind(&StatusBubbleViews::StatusView::OnTimer, 228 timer_factory_.GetWeakPtr()), 229 time); 230} 231 232void StatusBubbleViews::StatusView::OnTimer() { 233 if (stage_ == BUBBLE_HIDING_TIMER) { 234 stage_ = BUBBLE_HIDING_FADE; 235 StartFade(1.0, 0.0, kHideFadeDurationMS); 236 } else if (stage_ == BUBBLE_SHOWING_TIMER) { 237 stage_ = BUBBLE_SHOWING_FADE; 238 StartFade(0.0, 1.0, kShowFadeDurationMS); 239 } 240} 241 242void StatusBubbleViews::StatusView::CancelTimer() { 243 if (timer_factory_.HasWeakPtrs()) 244 timer_factory_.InvalidateWeakPtrs(); 245} 246 247void StatusBubbleViews::StatusView::RestartTimer(base::TimeDelta delay) { 248 CancelTimer(); 249 StartTimer(delay); 250} 251 252void StatusBubbleViews::StatusView::ResetTimer() { 253 if (stage_ == BUBBLE_SHOWING_TIMER) { 254 // We hadn't yet begun showing anything when we received a new request 255 // for something to show, so we start from scratch. 256 RestartTimer(base::TimeDelta::FromMilliseconds(kShowDelay)); 257 } 258} 259 260void StatusBubbleViews::StatusView::StartFade(double start, 261 double end, 262 int duration) { 263 opacity_start_ = start; 264 opacity_end_ = end; 265 266 // This will also reset the currently-occurring animation. 267 SetDuration(duration); 268 Start(); 269} 270 271void StatusBubbleViews::StatusView::StartHiding() { 272 if (stage_ == BUBBLE_SHOWN) { 273 stage_ = BUBBLE_HIDING_TIMER; 274 StartTimer(base::TimeDelta::FromMilliseconds(kHideDelay)); 275 } else if (stage_ == BUBBLE_SHOWING_TIMER) { 276 stage_ = BUBBLE_HIDDEN; 277 popup_->Hide(); 278 CancelTimer(); 279 } else if (stage_ == BUBBLE_SHOWING_FADE) { 280 stage_ = BUBBLE_HIDING_FADE; 281 // Figure out where we are in the current fade. 282 double current_opacity = GetCurrentOpacity(); 283 284 // Start a fade in the opposite direction. 285 StartFade(current_opacity, 0.0, 286 static_cast<int>(kHideFadeDurationMS * current_opacity)); 287 } 288} 289 290void StatusBubbleViews::StatusView::StartShowing() { 291 if (stage_ == BUBBLE_HIDDEN) { 292 popup_->Show(); 293 stage_ = BUBBLE_SHOWING_TIMER; 294 StartTimer(base::TimeDelta::FromMilliseconds(kShowDelay)); 295 } else if (stage_ == BUBBLE_HIDING_TIMER) { 296 stage_ = BUBBLE_SHOWN; 297 CancelTimer(); 298 } else if (stage_ == BUBBLE_HIDING_FADE) { 299 // We're partway through a fade. 300 stage_ = BUBBLE_SHOWING_FADE; 301 302 // Figure out where we are in the current fade. 303 double current_opacity = GetCurrentOpacity(); 304 305 // Start a fade in the opposite direction. 306 StartFade(current_opacity, 1.0, 307 static_cast<int>(kShowFadeDurationMS * current_opacity)); 308 } else if (stage_ == BUBBLE_SHOWING_TIMER) { 309 // We hadn't yet begun showing anything when we received a new request 310 // for something to show, so we start from scratch. 311 ResetTimer(); 312 } 313} 314 315// Animation functions. 316double StatusBubbleViews::StatusView::GetCurrentOpacity() { 317 return opacity_start_ + (opacity_end_ - opacity_start_) * 318 gfx::LinearAnimation::GetCurrentValue(); 319} 320 321void StatusBubbleViews::StatusView::SetOpacity(double opacity) { 322 popup_->SetOpacity(static_cast<unsigned char>(opacity * 255)); 323} 324 325void StatusBubbleViews::StatusView::AnimateToState(double state) { 326 SetOpacity(GetCurrentOpacity()); 327} 328 329void StatusBubbleViews::StatusView::AnimationEnded( 330 const gfx::Animation* animation) { 331 SetOpacity(opacity_end_); 332 333 if (stage_ == BUBBLE_HIDING_FADE) { 334 stage_ = BUBBLE_HIDDEN; 335 popup_->Hide(); 336 } else if (stage_ == BUBBLE_SHOWING_FADE) { 337 stage_ = BUBBLE_SHOWN; 338 } 339} 340 341void StatusBubbleViews::StatusView::SetStyle(BubbleStyle style) { 342 if (style_ != style) { 343 style_ = style; 344 SchedulePaint(); 345 } 346} 347 348void StatusBubbleViews::StatusView::OnPaint(gfx::Canvas* canvas) { 349 SkPaint paint; 350 paint.setStyle(SkPaint::kFill_Style); 351 paint.setAntiAlias(true); 352 SkColor toolbar_color = theme_service_->GetColor( 353 ThemeProperties::COLOR_TOOLBAR); 354 paint.setColor(toolbar_color); 355 356 gfx::Rect popup_bounds = popup_->GetWindowBoundsInScreen(); 357 358 // Figure out how to round the bubble's four corners. 359 SkScalar rad[8]; 360 361 // Top Edges - if the bubble is in its bottom position (sticking downwards), 362 // then we square the top edges. Otherwise, we square the edges based on the 363 // position of the bubble within the window (the bubble is positioned in the 364 // southeast corner in RTL and in the southwest corner in LTR). 365 if (style_ == STYLE_BOTTOM) { 366 // Top Left corner. 367 rad[0] = 0; 368 rad[1] = 0; 369 370 // Top Right corner. 371 rad[2] = 0; 372 rad[3] = 0; 373 } else { 374 if (base::i18n::IsRTL() != (style_ == STYLE_STANDARD_RIGHT)) { 375 // The text is RtL or the bubble is on the right side (but not both). 376 377 // Top Left corner. 378 rad[0] = SkIntToScalar(kBubbleCornerRadius); 379 rad[1] = SkIntToScalar(kBubbleCornerRadius); 380 381 // Top Right corner. 382 rad[2] = 0; 383 rad[3] = 0; 384 } else { 385 // Top Left corner. 386 rad[0] = 0; 387 rad[1] = 0; 388 389 // Top Right corner. 390 rad[2] = SkIntToScalar(kBubbleCornerRadius); 391 rad[3] = SkIntToScalar(kBubbleCornerRadius); 392 } 393 } 394 395 // Bottom edges - square these off if the bubble is in its standard position 396 // (sticking upward). 397 if (style_ == STYLE_STANDARD || style_ == STYLE_STANDARD_RIGHT) { 398 // Bottom Right Corner. 399 rad[4] = 0; 400 rad[5] = 0; 401 402 // Bottom Left Corner. 403 rad[6] = 0; 404 rad[7] = 0; 405 } else { 406 // Bottom Right Corner. 407 rad[4] = SkIntToScalar(kBubbleCornerRadius); 408 rad[5] = SkIntToScalar(kBubbleCornerRadius); 409 410 // Bottom Left Corner. 411 rad[6] = SkIntToScalar(kBubbleCornerRadius); 412 rad[7] = SkIntToScalar(kBubbleCornerRadius); 413 } 414 415 // Draw the bubble's shadow. 416 int width = popup_bounds.width(); 417 int height = popup_bounds.height(); 418 SkRect rect(gfx::RectToSkRect(gfx::Rect(popup_bounds.size()))); 419 SkPath shadow_path; 420 shadow_path.addRoundRect(rect, rad, SkPath::kCW_Direction); 421 SkPaint shadow_paint; 422 shadow_paint.setAntiAlias(true); 423 shadow_paint.setColor(kShadowColor); 424 canvas->DrawPath(shadow_path, shadow_paint); 425 426 // Draw the bubble. 427 rect.set(SkIntToScalar(kShadowThickness), 428 SkIntToScalar(kShadowThickness), 429 SkIntToScalar(width - kShadowThickness), 430 SkIntToScalar(height - kShadowThickness)); 431 SkPath path; 432 path.addRoundRect(rect, rad, SkPath::kCW_Direction); 433 canvas->DrawPath(path, paint); 434 435 // Draw highlight text and then the text body. In order to make sure the text 436 // is aligned to the right on RTL UIs, we mirror the text bounds if the 437 // locale is RTL. 438 int text_width = std::min( 439 views::Label::font().GetStringWidth(text_), 440 width - (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding); 441 int text_height = height - (kShadowThickness * 2); 442 gfx::Rect body_bounds(kShadowThickness + kTextPositionX, 443 kShadowThickness, 444 std::max(0, text_width), 445 std::max(0, text_height)); 446 body_bounds.set_x(GetMirroredXForRect(body_bounds)); 447 SkColor text_color = 448 theme_service_->GetColor(ThemeProperties::COLOR_STATUS_BAR_TEXT); 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 gfx::LinearAnimation, 463 public gfx::AnimationDelegate { 464 public: 465 StatusViewExpander(StatusBubbleViews* status_bubble, 466 StatusView* status_view) 467 : gfx::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 virtual void AnimateToState(double state) OVERRIDE; 487 virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE; 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 503void StatusBubbleViews::StatusViewExpander::AnimateToState(double state) { 504 SetBubbleWidth(GetCurrentBubbleWidth()); 505} 506 507void StatusBubbleViews::StatusViewExpander::AnimationEnded( 508 const gfx::Animation* animation) { 509 SetBubbleWidth(expansion_end_); 510 status_view_->SetText(expanded_text_, false); 511} 512 513void 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 527int StatusBubbleViews::StatusViewExpander::GetCurrentBubbleWidth() { 528 return static_cast<int>(expansion_start_ + 529 (expansion_end_ - expansion_start_) * 530 gfx::LinearAnimation::GetCurrentValue()); 531} 532 533void StatusBubbleViews::StatusViewExpander::SetBubbleWidth(int width) { 534 status_bubble_->SetBubbleWidth(width); 535 status_view_->SchedulePaint(); 536} 537 538// StatusBubble --------------------------------------------------------------- 539 540const int StatusBubbleViews::kShadowThickness = 1; 541 542StatusBubbleViews::StatusBubbleViews(views::View* base_view) 543 : contains_mouse_(false), 544 offset_(0), 545 opacity_(0), 546 base_view_(base_view), 547 view_(NULL), 548 download_shelf_is_visible_(false), 549 is_expanded_(false), 550 expand_timer_factory_(this) { 551 expand_view_.reset(); 552} 553 554StatusBubbleViews::~StatusBubbleViews() { 555 CancelExpandTimer(); 556 if (popup_.get()) 557 popup_->CloseNow(); 558} 559 560void StatusBubbleViews::Init() { 561 if (!popup_.get()) { 562 popup_.reset(new views::Widget); 563 views::Widget* frame = base_view_->GetWidget(); 564 if (!view_) 565 view_ = new StatusView(this, popup_.get(), frame->GetThemeProvider()); 566 if (!expand_view_.get()) 567 expand_view_.reset(new StatusViewExpander(this, view_)); 568 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); 569 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 570 params.accept_events = false; 571 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 572 params.parent = frame->GetNativeView(); 573 params.context = frame->GetNativeView(); 574 popup_->Init(params); 575 // We do our own animation and don't want any from the system. 576 popup_->SetVisibilityChangedAnimationsEnabled(false); 577 popup_->SetOpacity(0x00); 578 popup_->SetContentsView(view_); 579#if defined(USE_ASH) 580 ash::wm::GetWindowState(popup_->GetNativeWindow())-> 581 set_ignored_by_shelf(true); 582#endif 583 Reposition(); 584 } 585} 586 587void StatusBubbleViews::Reposition() { 588 if (popup_.get()) { 589 gfx::Point top_left; 590 views::View::ConvertPointToScreen(base_view_, &top_left); 591 592 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 593 top_left.y() + position_.y(), 594 size_.width(), size_.height())); 595 } 596} 597 598gfx::Size StatusBubbleViews::GetPreferredSize() { 599 return gfx::Size(0, ui::ResourceBundle::GetSharedInstance().GetFont( 600 ui::ResourceBundle::BaseFont).GetHeight() + kTotalVerticalPadding); 601} 602 603void StatusBubbleViews::SetBounds(int x, int y, int w, int h) { 604 original_position_.SetPoint(x, y); 605 position_.SetPoint(base_view_->GetMirroredXWithWidthInView(x, w), y); 606 size_.SetSize(w, h); 607 Reposition(); 608 if (popup_.get() && contains_mouse_) 609 AvoidMouse(last_mouse_moved_location_); 610} 611 612void StatusBubbleViews::SetStatus(const string16& status_text) { 613 if (size_.IsEmpty()) 614 return; // We have no bounds, don't attempt to show the popup. 615 616 if (status_text_ == status_text && !status_text.empty()) 617 return; 618 619 if (!IsFrameVisible()) 620 return; // Don't show anything if the parent isn't visible. 621 622 Init(); 623 status_text_ = status_text; 624 if (!status_text_.empty()) { 625 view_->SetText(status_text, true); 626 view_->Show(); 627 } else if (!url_text_.empty()) { 628 view_->SetText(url_text_, true); 629 } else { 630 view_->SetText(string16(), true); 631 } 632} 633 634void StatusBubbleViews::SetURL(const GURL& url, const std::string& languages) { 635 url_ = url; 636 languages_ = languages; 637 if (size_.IsEmpty()) 638 return; // We have no bounds, don't attempt to show the popup. 639 640 Init(); 641 642 // If we want to clear a displayed URL but there is a status still to 643 // display, display that status instead. 644 if (url.is_empty() && !status_text_.empty()) { 645 url_text_ = string16(); 646 if (IsFrameVisible()) 647 view_->SetText(status_text_, true); 648 return; 649 } 650 651 // Reset expansion state only when bubble is completely hidden. 652 if (view_->GetState() == StatusView::BUBBLE_HIDDEN) { 653 is_expanded_ = false; 654 SetBubbleWidth(GetStandardStatusBubbleWidth()); 655 } 656 657 // Set Elided Text corresponding to the GURL object. 658 gfx::Rect popup_bounds = popup_->GetWindowBoundsInScreen(); 659 int text_width = static_cast<int>(popup_bounds.width() - 660 (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1); 661 url_text_ = gfx::ElideUrl(url, view_->Label::font_list(), text_width, 662 languages); 663 664 // An URL is always treated as a left-to-right string. On right-to-left UIs 665 // we need to explicitly mark the URL as LTR to make sure it is displayed 666 // correctly. 667 url_text_ = base::i18n::GetDisplayStringInLTRDirectionality(url_text_); 668 669 if (IsFrameVisible()) { 670 view_->SetText(url_text_, true); 671 672 CancelExpandTimer(); 673 674 // If bubble is already in expanded state, shift to adjust to new text 675 // size (shrinking or expanding). Otherwise delay. 676 if (is_expanded_ && !url.is_empty()) { 677 ExpandBubble(); 678 } else if (net::FormatUrl(url, languages).length() > url_text_.length()) { 679 base::MessageLoop::current()->PostDelayedTask( 680 FROM_HERE, 681 base::Bind(&StatusBubbleViews::ExpandBubble, 682 expand_timer_factory_.GetWeakPtr()), 683 base::TimeDelta::FromMilliseconds(kExpandHoverDelay)); 684 } 685 } 686} 687 688void StatusBubbleViews::Hide() { 689 status_text_ = string16(); 690 url_text_ = string16(); 691 if (view_) 692 view_->Hide(); 693} 694 695void StatusBubbleViews::MouseMoved(const gfx::Point& location, 696 bool left_content) { 697 contains_mouse_ = !left_content; 698 if (left_content) { 699 Reposition(); 700 return; 701 } 702 last_mouse_moved_location_ = location; 703 704 if (view_) { 705 view_->ResetTimer(); 706 707 if (view_->GetState() != StatusView::BUBBLE_HIDDEN && 708 view_->GetState() != StatusView::BUBBLE_HIDING_FADE && 709 view_->GetState() != StatusView::BUBBLE_HIDING_TIMER) { 710 AvoidMouse(location); 711 } 712 } 713} 714 715void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) { 716 download_shelf_is_visible_ = visible; 717} 718 719void StatusBubbleViews::AvoidMouse(const gfx::Point& location) { 720 // Get the position of the frame. 721 gfx::Point top_left; 722 views::View::ConvertPointToScreen(base_view_, &top_left); 723 // Border included. 724 int window_width = base_view_->GetLocalBounds().width(); 725 726 // Get the cursor position relative to the popup. 727 gfx::Point relative_location = location; 728 if (base::i18n::IsRTL()) { 729 int top_right_x = top_left.x() + window_width; 730 relative_location.set_x(top_right_x - relative_location.x()); 731 } else { 732 relative_location.set_x( 733 relative_location.x() - (top_left.x() + position_.x())); 734 } 735 relative_location.set_y( 736 relative_location.y() - (top_left.y() + position_.y())); 737 738 // If the mouse is in a position where we think it would move the 739 // status bubble, figure out where and how the bubble should be moved. 740 if (relative_location.y() > -kMousePadding && 741 relative_location.x() < size_.width() + kMousePadding) { 742 int offset = kMousePadding + relative_location.y(); 743 744 // Make the movement non-linear. 745 offset = offset * offset / kMousePadding; 746 747 // When the mouse is entering from the right, we want the offset to be 748 // scaled by how horizontally far away the cursor is from the bubble. 749 if (relative_location.x() > size_.width()) { 750 offset = static_cast<int>(static_cast<float>(offset) * ( 751 static_cast<float>(kMousePadding - 752 (relative_location.x() - size_.width())) / 753 static_cast<float>(kMousePadding))); 754 } 755 756 // Cap the offset and change the visual presentation of the bubble 757 // depending on where it ends up (so that rounded corners square off 758 // and mate to the edges of the tab content). 759 if (offset >= size_.height() - kShadowThickness * 2) { 760 offset = size_.height() - kShadowThickness * 2; 761 view_->SetStyle(StatusView::STYLE_BOTTOM); 762 } else if (offset > kBubbleCornerRadius / 2 - kShadowThickness) { 763 view_->SetStyle(StatusView::STYLE_FLOATING); 764 } else { 765 view_->SetStyle(StatusView::STYLE_STANDARD); 766 } 767 768 // Check if the bubble sticks out from the monitor or will obscure 769 // download shelf. 770 gfx::NativeView window = base_view_->GetWidget()->GetNativeView(); 771 gfx::Rect monitor_rect = gfx::Screen::GetScreenFor(window)-> 772 GetDisplayNearestWindow(window).work_area(); 773 const int bubble_bottom_y = top_left.y() + position_.y() + size_.height(); 774 775 if (bubble_bottom_y + offset > monitor_rect.height() || 776 (download_shelf_is_visible_ && 777 (view_->GetStyle() == StatusView::STYLE_FLOATING || 778 view_->GetStyle() == StatusView::STYLE_BOTTOM))) { 779 // The offset is still too large. Move the bubble to the right and reset 780 // Y offset_ to zero. 781 view_->SetStyle(StatusView::STYLE_STANDARD_RIGHT); 782 offset_ = 0; 783 784 // Subtract border width + bubble width. 785 int right_position_x = window_width - (position_.x() + size_.width()); 786 popup_->SetBounds(gfx::Rect(top_left.x() + right_position_x, 787 top_left.y() + position_.y(), 788 size_.width(), size_.height())); 789 } else { 790 offset_ = offset; 791 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 792 top_left.y() + position_.y() + offset_, 793 size_.width(), size_.height())); 794 } 795 } else if (offset_ != 0 || 796 view_->GetStyle() == StatusView::STYLE_STANDARD_RIGHT) { 797 offset_ = 0; 798 view_->SetStyle(StatusView::STYLE_STANDARD); 799 popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), 800 top_left.y() + position_.y(), 801 size_.width(), size_.height())); 802 } 803} 804 805bool StatusBubbleViews::IsFrameVisible() { 806 views::Widget* frame = base_view_->GetWidget(); 807 if (!frame->IsVisible()) 808 return false; 809 810 views::Widget* window = frame->GetTopLevelWidget(); 811 return !window || !window->IsMinimized(); 812} 813 814void StatusBubbleViews::ExpandBubble() { 815 // Elide URL to maximum possible size, then check actual length (it may 816 // still be too long to fit) before expanding bubble. 817 gfx::Rect popup_bounds = popup_->GetWindowBoundsInScreen(); 818 int max_status_bubble_width = GetMaxStatusBubbleWidth(); 819 url_text_ = gfx::ElideUrl(url_, view_->Label::font_list(), 820 max_status_bubble_width, languages_); 821 int expanded_bubble_width =std::max(GetStandardStatusBubbleWidth(), 822 std::min(view_->Label::font().GetStringWidth(url_text_) + 823 (kShadowThickness * 2) + kTextPositionX + 824 kTextHorizPadding + 1, 825 max_status_bubble_width)); 826 is_expanded_ = true; 827 expand_view_->StartExpansion(url_text_, popup_bounds.width(), 828 expanded_bubble_width); 829} 830 831int StatusBubbleViews::GetStandardStatusBubbleWidth() { 832 return base_view_->bounds().width() / 3; 833} 834 835int StatusBubbleViews::GetMaxStatusBubbleWidth() { 836 const ui::NativeTheme* theme = base_view_->GetNativeTheme(); 837 return static_cast<int>(std::max(0, base_view_->bounds().width() - 838 (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1 - 839 views::NativeScrollBar::GetVerticalScrollBarWidth(theme))); 840} 841 842void StatusBubbleViews::SetBubbleWidth(int width) { 843 size_.set_width(width); 844 SetBounds(original_position_.x(), original_position_.y(), 845 size_.width(), size_.height()); 846} 847 848void StatusBubbleViews::CancelExpandTimer() { 849 if (expand_timer_factory_.HasWeakPtrs()) 850 expand_timer_factory_.InvalidateWeakPtrs(); 851} 852