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