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