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