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