status_bubble_views.cc revision dc0f95d653279beabeb9817299e2902918ba123e
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/browser_theme_provider.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
34using views::Widget;
35
36// The alpha and color of the bubble's shadow.
37static const SkColor kShadowColor = SkColorSetARGB(30, 0, 0, 0);
38
39// The roundedness of the edges of our bubble.
40static const int kBubbleCornerRadius = 4;
41
42// How close the mouse can get to the infobubble before it starts sliding
43// off-screen.
44static const int kMousePadding = 20;
45
46// The horizontal offset of the text within the status bubble, not including the
47// outer shadow ring.
48static 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.
52static const int kTextHorizPadding = 1;
53
54// Delays before we start hiding or showing the bubble after we receive a
55// show or hide request.
56static const int kShowDelay = 80;
57static const int kHideDelay = 250;
58
59// How long each fade should last for.
60static const int kShowFadeDurationMS = 120;
61static const int kHideFadeDurationMS = 200;
62static const int kFramerate = 25;
63
64// How long each expansion step should take.
65static const int kMinExpansionStepDurationMS = 20;
66static 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.
71class StatusBubbleViews::StatusView : public views::Label,
72                                      public ui::LinearAnimation,
73                                      public ui::AnimationDelegate {
74 public:
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_provider_(theme_provider) {
86    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
87    gfx::Font font(rb.GetFont(ResourceBundle::BaseFont));
88    SetFont(font);
89  }
90
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
118  BubbleStage GetState() const { return stage_; }
119
120  void SetStyle(BubbleStyle style);
121
122  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_provider_;
178};
179
180void 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
195void StatusBubbleViews::StatusView::Show() {
196  Stop();
197  CancelTimer();
198  SetOpacity(1.0);
199  popup_->Show();
200  stage_ = BUBBLE_SHOWN;
201  PaintNow();
202}
203
204void StatusBubbleViews::StatusView::Hide() {
205  Stop();
206  CancelTimer();
207  SetOpacity(0.0);
208  text_.clear();
209  popup_->Hide();
210  stage_ = BUBBLE_HIDDEN;
211}
212
213void 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
222void 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
232void StatusBubbleViews::StatusView::CancelTimer() {
233  if (!timer_factory_.empty())
234    timer_factory_.RevokeAll();
235}
236
237void StatusBubbleViews::StatusView::RestartTimer(int delay) {
238  CancelTimer();
239  StartTimer(delay);
240}
241
242void 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
250void 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
261void 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
279void 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.
305double StatusBubbleViews::StatusView::GetCurrentOpacity() {
306  return opacity_start_ + (opacity_end_ - opacity_start_) *
307         ui::LinearAnimation::GetCurrentValue();
308}
309
310void StatusBubbleViews::StatusView::SetOpacity(double opacity) {
311  popup_->SetOpacity(static_cast<unsigned char>(opacity * 255));
312  SchedulePaint();
313}
314
315void StatusBubbleViews::StatusView::AnimateToState(double state) {
316  SetOpacity(GetCurrentOpacity());
317}
318
319void 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
331void StatusBubbleViews::StatusView::SetStyle(BubbleStyle style) {
332  if (style_ != style) {
333    style_ = style;
334    SchedulePaint();
335  }
336}
337
338void 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_provider_->GetColor(BrowserThemeProvider::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_provider_->GetColor(BrowserThemeProvider::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.
462class StatusBubbleViews::StatusViewExpander : public ui::LinearAnimation,
463                                              public ui::AnimationDelegate {
464 public:
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(string16 expanded_text, int current_width,
476                      int expansion_end);
477
478  // Set width of fully expanded bubble.
479  void SetExpandedWidth(int expanded_width);
480
481 private:
482  // Animation functions.
483  int GetCurrentBubbleWidth();
484  void SetBubbleWidth(int width);
485  void AnimateToState(double state);
486  void AnimationEnded(const ui::Animation* animation);
487
488  // Manager that owns us.
489  StatusBubbleViews* status_bubble_;
490
491  // Change the bounds and text of this view.
492  StatusView* status_view_;
493
494  // Text elided (if needed) to fit maximum status bar width.
495  string16 expanded_text_;
496
497  // Widths at expansion start and end.
498  int expansion_start_;
499  int expansion_end_;
500};
501
502void StatusBubbleViews::StatusViewExpander::AnimateToState(double state) {
503  SetBubbleWidth(GetCurrentBubbleWidth());
504}
505
506void StatusBubbleViews::StatusViewExpander::AnimationEnded(
507    const ui::Animation* animation) {
508  SetBubbleWidth(expansion_end_);
509  status_view_->SetText(expanded_text_, false);
510}
511
512void StatusBubbleViews::StatusViewExpander::StartExpansion(
513    string16 expanded_text, int expansion_start,
514    int expansion_end) {
515  expanded_text_ = expanded_text;
516  expansion_start_ = expansion_start;
517  expansion_end_ = expansion_end;
518  int min_duration = std::max(kMinExpansionStepDurationMS,
519      static_cast<int>(kMaxExpansionStepDurationMS *
520          (expansion_end - expansion_start) / 100.0));
521  SetDuration(std::min(kMaxExpansionStepDurationMS, min_duration));
522  Start();
523}
524
525int StatusBubbleViews::StatusViewExpander::GetCurrentBubbleWidth() {
526  return static_cast<int>(expansion_start_ +
527      (expansion_end_ - expansion_start_) *
528          ui::LinearAnimation::GetCurrentValue());
529}
530
531void StatusBubbleViews::StatusViewExpander::SetBubbleWidth(int width) {
532  status_bubble_->SetBubbleWidth(width);
533  status_view_->SchedulePaint();
534}
535
536// StatusBubble ---------------------------------------------------------------
537
538const int StatusBubbleViews::kShadowThickness = 1;
539
540StatusBubbleViews::StatusBubbleViews(views::View* base_view)
541    : offset_(0),
542      popup_(NULL),
543      opacity_(0),
544      base_view_(base_view),
545      view_(NULL),
546      download_shelf_is_visible_(false),
547      is_expanded_(false),
548      ALLOW_THIS_IN_INITIALIZER_LIST(expand_timer_factory_(this)) {
549  expand_view_.reset();
550}
551
552StatusBubbleViews::~StatusBubbleViews() {
553  CancelExpandTimer();
554  if (popup_.get())
555    popup_->CloseNow();
556}
557
558void StatusBubbleViews::Init() {
559  if (!popup_.get()) {
560    popup_.reset(Widget::CreatePopupWidget(Widget::Transparent,
561                                           Widget::NotAcceptEvents,
562                                           Widget::NotDeleteOnDestroy,
563                                           Widget::MirrorOriginInRTL));
564    views::Widget* frame = base_view_->GetWidget();
565    if (!view_)
566      view_ = new StatusView(this, popup_.get(), frame->GetThemeProvider());
567    if (!expand_view_.get())
568      expand_view_.reset(new StatusViewExpander(this, view_));
569    popup_->SetOpacity(0x00);
570    popup_->Init(frame->GetNativeView(), gfx::Rect());
571    popup_->SetContentsView(view_);
572    Reposition();
573    popup_->Show();
574  }
575}
576
577void StatusBubbleViews::Reposition() {
578  if (popup_.get()) {
579    gfx::Point top_left;
580    views::View::ConvertPointToScreen(base_view_, &top_left);
581
582    popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(),
583                                top_left.y() + position_.y(),
584                                size_.width(), size_.height()));
585  }
586}
587
588gfx::Size StatusBubbleViews::GetPreferredSize() {
589  return gfx::Size(0, ResourceBundle::GetSharedInstance().GetFont(
590      ResourceBundle::BaseFont).GetHeight() + kTotalVerticalPadding);
591}
592
593void StatusBubbleViews::SetBounds(int x, int y, int w, int h) {
594  original_position_.SetPoint(x, y);
595  position_.SetPoint(base_view_->GetMirroredXWithWidthInView(x, w), y);
596  size_.SetSize(w, h);
597  Reposition();
598}
599
600void StatusBubbleViews::SetStatus(const string16& status_text) {
601  if (size_.IsEmpty())
602    return;  // We have no bounds, don't attempt to show the popup.
603
604  if (status_text_ == status_text && !status_text.empty())
605    return;
606
607  if (!IsFrameVisible())
608    return;  // Don't show anything if the parent isn't visible.
609
610  Init();
611  status_text_ = status_text;
612  if (!status_text_.empty()) {
613    view_->SetText(status_text, true);
614    view_->Show();
615  } else if (!url_text_.empty()) {
616    view_->SetText(url_text_, true);
617  } else {
618    view_->SetText(string16(), true);
619  }
620}
621
622void StatusBubbleViews::SetURL(const GURL& url, const string16& languages) {
623  languages_ = languages;
624  url_ = url;
625  if (size_.IsEmpty())
626    return;  // We have no bounds, don't attempt to show the popup.
627
628  Init();
629
630  // If we want to clear a displayed URL but there is a status still to
631  // display, display that status instead.
632  if (url.is_empty() && !status_text_.empty()) {
633    url_text_ = string16();
634    if (IsFrameVisible())
635      view_->SetText(status_text_, true);
636    return;
637  }
638
639  // Reset expansion state only when bubble is completely hidden.
640  if (view_->GetState() == StatusView::BUBBLE_HIDDEN) {
641    is_expanded_ = false;
642    SetBubbleWidth(GetStandardStatusBubbleWidth());
643  }
644
645  // Set Elided Text corresponding to the GURL object.
646  gfx::Rect popup_bounds = popup_->GetWindowScreenBounds();
647  int text_width = static_cast<int>(popup_bounds.width() -
648      (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1);
649  url_text_ = ui::ElideUrl(url, view_->Label::font(),
650      text_width, UTF16ToUTF8(languages));
651
652  std::wstring original_url_text =
653      UTF16ToWideHack(net::FormatUrl(url, UTF16ToUTF8(languages)));
654
655  // An URL is always treated as a left-to-right string. On right-to-left UIs
656  // we need to explicitly mark the URL as LTR to make sure it is displayed
657  // correctly.
658  url_text_ = base::i18n::GetDisplayStringInLTRDirectionality(url_text_);
659
660  if (IsFrameVisible()) {
661    view_->SetText(url_text_, true);
662
663    CancelExpandTimer();
664
665    // If bubble is already in expanded state, shift to adjust to new text
666    // size (shrinking or expanding). Otherwise delay.
667    if (is_expanded_ && !url.is_empty())
668      ExpandBubble();
669    else if (original_url_text.length() > url_text_.length())
670      MessageLoop::current()->PostDelayedTask(FROM_HERE,
671          expand_timer_factory_.NewRunnableMethod(
672          &StatusBubbleViews::ExpandBubble), kExpandHoverDelay);
673  }
674}
675
676void StatusBubbleViews::Hide() {
677  status_text_ = string16();
678  url_text_ = string16();
679  if (view_)
680    view_->Hide();
681}
682
683void StatusBubbleViews::MouseMoved(const gfx::Point& location,
684                                   bool left_content) {
685  if (left_content)
686    return;
687
688  if (view_) {
689    view_->ResetTimer();
690
691    if (view_->GetState() != StatusView::BUBBLE_HIDDEN &&
692        view_->GetState() != StatusView::BUBBLE_HIDING_FADE &&
693        view_->GetState() != StatusView::BUBBLE_HIDING_TIMER) {
694      AvoidMouse(location);
695    }
696  }
697}
698
699void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) {
700  download_shelf_is_visible_ = visible;
701}
702
703void StatusBubbleViews::AvoidMouse(const gfx::Point& location) {
704  // Get the position of the frame.
705  gfx::Point top_left;
706  views::View::ConvertPointToScreen(base_view_, &top_left);
707  // Border included.
708  int window_width = base_view_->GetLocalBounds().width();
709
710  // Get the cursor position relative to the popup.
711  gfx::Point relative_location = location;
712  if (base::i18n::IsRTL()) {
713    int top_right_x = top_left.x() + window_width;
714    relative_location.set_x(top_right_x - relative_location.x());
715  } else {
716    relative_location.set_x(
717        relative_location.x() - (top_left.x() + position_.x()));
718  }
719  relative_location.set_y(
720      relative_location.y() - (top_left.y() + position_.y()));
721
722  // If the mouse is in a position where we think it would move the
723  // status bubble, figure out where and how the bubble should be moved.
724  if (relative_location.y() > -kMousePadding &&
725      relative_location.x() < size_.width() + kMousePadding) {
726    int offset = kMousePadding + relative_location.y();
727
728    // Make the movement non-linear.
729    offset = offset * offset / kMousePadding;
730
731    // When the mouse is entering from the right, we want the offset to be
732    // scaled by how horizontally far away the cursor is from the bubble.
733    if (relative_location.x() > size_.width()) {
734      offset = static_cast<int>(static_cast<float>(offset) * (
735          static_cast<float>(kMousePadding -
736              (relative_location.x() - size_.width())) /
737          static_cast<float>(kMousePadding)));
738    }
739
740    // Cap the offset and change the visual presentation of the bubble
741    // depending on where it ends up (so that rounded corners square off
742    // and mate to the edges of the tab content).
743    if (offset >= size_.height() - kShadowThickness * 2) {
744      offset = size_.height() - kShadowThickness * 2;
745      view_->SetStyle(StatusView::STYLE_BOTTOM);
746    } else if (offset > kBubbleCornerRadius / 2 - kShadowThickness) {
747      view_->SetStyle(StatusView::STYLE_FLOATING);
748    } else {
749      view_->SetStyle(StatusView::STYLE_STANDARD);
750    }
751
752    // Check if the bubble sticks out from the monitor or will obscure
753    // download shelf.
754    gfx::NativeView widget = base_view_->GetWidget()->GetNativeView();
755    gfx::Rect monitor_rect =
756        views::Screen::GetMonitorWorkAreaNearestWindow(widget);
757    const int bubble_bottom_y = top_left.y() + position_.y() + size_.height();
758
759    if (bubble_bottom_y + offset > monitor_rect.height() ||
760        (download_shelf_is_visible_ &&
761         (view_->GetStyle() == StatusView::STYLE_FLOATING ||
762          view_->GetStyle() == StatusView::STYLE_BOTTOM))) {
763      // The offset is still too large. Move the bubble to the right and reset
764      // Y offset_ to zero.
765      view_->SetStyle(StatusView::STYLE_STANDARD_RIGHT);
766      offset_ = 0;
767
768      // Subtract border width + bubble width.
769      int right_position_x = window_width - (position_.x() + size_.width());
770      popup_->SetBounds(gfx::Rect(top_left.x() + right_position_x,
771                                  top_left.y() + position_.y(),
772                                  size_.width(), size_.height()));
773    } else {
774      offset_ = offset;
775      popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(),
776                                  top_left.y() + position_.y() + offset_,
777                                  size_.width(), size_.height()));
778    }
779  } else if (offset_ != 0 ||
780      view_->GetStyle() == StatusView::STYLE_STANDARD_RIGHT) {
781    offset_ = 0;
782    view_->SetStyle(StatusView::STYLE_STANDARD);
783    popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(),
784                                top_left.y() + position_.y(),
785                                size_.width(), size_.height()));
786  }
787}
788
789bool StatusBubbleViews::IsFrameVisible() {
790  views::Widget* frame = base_view_->GetWidget();
791  if (!frame->IsVisible())
792    return false;
793
794  views::Window* window = frame->GetWindow();
795  return !window || !window->IsMinimized();
796}
797
798void StatusBubbleViews::ExpandBubble() {
799  // Elide URL to maximum possible size, then check actual length (it may
800  // still be too long to fit) before expanding bubble.
801  gfx::Rect popup_bounds = popup_->GetWindowScreenBounds();
802  int max_status_bubble_width = GetMaxStatusBubbleWidth();
803  url_text_ = ui::ElideUrl(url_, view_->Label::font(),
804      max_status_bubble_width, UTF16ToUTF8(languages_));
805  int expanded_bubble_width =std::max(GetStandardStatusBubbleWidth(),
806      std::min(view_->Label::font().GetStringWidth(url_text_) +
807                   (kShadowThickness * 2) + kTextPositionX +
808                   kTextHorizPadding + 1,
809               max_status_bubble_width));
810  is_expanded_ = true;
811  expand_view_->StartExpansion(url_text_, popup_bounds.width(),
812                               expanded_bubble_width);
813}
814
815int StatusBubbleViews::GetStandardStatusBubbleWidth() {
816  return base_view_->bounds().width() / 3;
817}
818
819int StatusBubbleViews::GetMaxStatusBubbleWidth() {
820  return static_cast<int>(std::max(0, base_view_->bounds().width() -
821      (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1 -
822      views::NativeScrollBar::GetVerticalScrollBarWidth()));
823}
824
825void StatusBubbleViews::SetBubbleWidth(int width) {
826  size_.set_width(width);
827  SetBounds(original_position_.x(), original_position_.y(),
828            size_.width(), size_.height());
829}
830
831void StatusBubbleViews::CancelExpandTimer() {
832  if (!expand_timer_factory_.empty())
833    expand_timer_factory_.RevokeAll();
834}
835