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.
39static const SkColor kShadowColor = SkColorSetARGB(30, 0, 0, 0);
40
41// The roundedness of the edges of our bubble.
42static const int kBubbleCornerRadius = 4;
43
44// How close the mouse can get to the infobubble before it starts sliding
45// off-screen.
46static const int kMousePadding = 20;
47
48// The horizontal offset of the text within the status bubble, not including the
49// outer shadow ring.
50static 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.
54static const int kTextHorizPadding = 1;
55
56// Delays before we start hiding or showing the bubble after we receive a
57// show or hide request.
58static const int kShowDelay = 80;
59static const int kHideDelay = 250;
60
61// How long each fade should last for.
62static const int kShowFadeDurationMS = 120;
63static const int kHideFadeDurationMS = 200;
64static const int kFramerate = 25;
65
66// How long each expansion step should take.
67static const int kMinExpansionStepDurationMS = 20;
68static const int kMaxExpansionStepDurationMS = 150;
69
70
71// StatusBubbleViews::StatusViewAnimation --------------------------------------
72class 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.
105class 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
190StatusBubbleViews::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
200StatusBubbleViews::StatusView::~StatusView() {
201  animation_->Stop();
202  CancelTimer();
203}
204
205void 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
221void StatusBubbleViews::StatusView::Show() {
222  animation_->Stop();
223  CancelTimer();
224  SetOpacity(1.0);
225  popup_->ShowInactive();
226  state_ = BUBBLE_SHOWN;
227}
228
229void StatusBubbleViews::StatusView::Hide() {
230  animation_->Stop();
231  CancelTimer();
232  SetOpacity(0.0);
233  text_.clear();
234  popup_->Hide();
235  state_ = BUBBLE_HIDDEN;
236}
237
238void 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
249void 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
259void StatusBubbleViews::StatusView::CancelTimer() {
260  if (timer_factory_.HasWeakPtrs())
261    timer_factory_.InvalidateWeakPtrs();
262}
263
264void StatusBubbleViews::StatusView::RestartTimer(base::TimeDelta delay) {
265  CancelTimer();
266  StartTimer(delay);
267}
268
269void 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
277void 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
287void 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
306void 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
331void StatusBubbleViews::StatusView::SetOpacity(double opacity) {
332  popup_->SetOpacity(static_cast<unsigned char>(opacity * 255));
333}
334
335void StatusBubbleViews::StatusView::SetStyle(BubbleStyle style) {
336  if (style_ != style) {
337    style_ = style;
338    SchedulePaint();
339  }
340}
341
342void 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
351void 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
434StatusBubbleViews::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
444StatusBubbleViews::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
450double StatusBubbleViews::StatusViewAnimation::GetCurrentOpacity() {
451  return opacity_start_ + (opacity_end_ - opacity_start_) *
452      gfx::LinearAnimation::GetCurrentValue();
453}
454
455void StatusBubbleViews::StatusViewAnimation::AnimateToState(double state) {
456  status_view_->SetOpacity(GetCurrentOpacity());
457}
458
459void 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.
470class 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
511void StatusBubbleViews::StatusViewExpander::AnimateToState(double state) {
512  SetBubbleWidth(GetCurrentBubbleWidth());
513}
514
515void StatusBubbleViews::StatusViewExpander::AnimationEnded(
516    const gfx::Animation* animation) {
517  SetBubbleWidth(expansion_end_);
518  status_view_->SetText(expanded_text_, false);
519}
520
521void 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
535int StatusBubbleViews::StatusViewExpander::GetCurrentBubbleWidth() {
536  return static_cast<int>(expansion_start_ +
537      (expansion_end_ - expansion_start_) *
538          gfx::LinearAnimation::GetCurrentValue());
539}
540
541void StatusBubbleViews::StatusViewExpander::SetBubbleWidth(int width) {
542  status_bubble_->SetBubbleWidth(width);
543  status_view_->SchedulePaint();
544}
545
546
547// StatusBubbleViews -----------------------------------------------------------
548
549const int StatusBubbleViews::kShadowThickness = 1;
550
551StatusBubbleViews::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
562StatusBubbleViews::~StatusBubbleViews() {
563  CancelExpandTimer();
564  if (popup_.get())
565    popup_->CloseNow();
566}
567
568void 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
594void 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
604void 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
615gfx::Size StatusBubbleViews::GetPreferredSize() {
616  return gfx::Size(0, gfx::FontList().GetHeight() + kTotalVerticalPadding);
617}
618
619void 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
628void 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
650void 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
703void StatusBubbleViews::Hide() {
704  status_text_ = base::string16();
705  url_text_ = base::string16();
706  if (view_)
707    view_->Hide();
708}
709
710void 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
730void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) {
731  download_shelf_is_visible_ = visible;
732}
733
734void 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
820bool 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
829bool StatusBubbleViews::IsFrameMaximized() {
830  views::Widget* frame = base_view_->GetWidget();
831  views::Widget* window = frame->GetTopLevelWidget();
832  return window && window->IsMaximized();
833}
834
835void 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
853int StatusBubbleViews::GetStandardStatusBubbleWidth() {
854  return base_view_->bounds().width() / 3;
855}
856
857int 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
864void StatusBubbleViews::SetBubbleWidth(int width) {
865  size_.set_width(width);
866  SetBounds(original_position_.x(), original_position_.y(),
867            size_.width(), size_.height());
868}
869
870void StatusBubbleViews::CancelExpandTimer() {
871  if (expand_timer_factory_.HasWeakPtrs())
872    expand_timer_factory_.InvalidateWeakPtrs();
873}
874