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