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