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