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 #include <utility>
9 
10 #include "base/bind.h"
11 #include "base/i18n/rtl.h"
12 #include "base/location.h"
13 #include "base/macros.h"
14 #include "base/single_thread_task_runner.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "base/threading/thread_task_runner_handle.h"
18 #include "base/timer/timer.h"
19 #include "build/build_config.h"
20 #include "cc/paint/paint_flags.h"
21 #include "chrome/browser/themes/theme_properties.h"
22 #include "components/url_formatter/elide_url.h"
23 #include "components/url_formatter/url_formatter.h"
24 #include "third_party/skia/include/core/SkPath.h"
25 #include "third_party/skia/include/pathops/SkPathOps.h"
26 #include "ui/base/theme_provider.h"
27 #include "ui/display/display.h"
28 #include "ui/display/screen.h"
29 #include "ui/gfx/animation/linear_animation.h"
30 #include "ui/gfx/canvas.h"
31 #include "ui/gfx/font_list.h"
32 #include "ui/gfx/geometry/point.h"
33 #include "ui/gfx/geometry/rect.h"
34 #include "ui/gfx/scoped_canvas.h"
35 #include "ui/gfx/skia_util.h"
36 #include "ui/gfx/text_elider.h"
37 #include "ui/gfx/text_utils.h"
38 #include "ui/native_theme/native_theme.h"
39 #include "ui/views/animation/animation_delegate_views.h"
40 #include "ui/views/controls/label.h"
41 #include "ui/views/controls/scrollbar/scroll_bar_views.h"
42 #include "ui/views/layout/fill_layout.h"
43 #include "ui/views/style/typography.h"
44 #include "ui/views/widget/root_view.h"
45 #include "ui/views/widget/widget.h"
46 #include "url/gurl.h"
47 
48 #if defined(OS_CHROMEOS)
49 #include "ash/public/cpp/window_properties.h"
50 #include "ui/aura/window.h"
51 #endif
52 
53 namespace {
54 
55 // The alpha and color of the bubble's shadow.
56 constexpr SkColor kShadowColor = SkColorSetARGB(30, 0, 0, 0);
57 
58 // The roundedness of the edges of our bubble.
59 constexpr int kBubbleCornerRadius = 4;
60 
61 // How close the mouse can get to the infobubble before it starts sliding
62 // off-screen.
63 constexpr int kMousePadding = 20;
64 
65 // The minimum horizontal space between the edges of the text and the edges of
66 // the status bubble, not including the outer shadow ring.
67 constexpr int kTextHorizPadding = 5;
68 
69 // Delays before we start hiding or showing the bubble after we receive a
70 // show or hide request.
71 constexpr auto kShowDelay = base::TimeDelta::FromMilliseconds(80);
72 constexpr auto kHideDelay = base::TimeDelta::FromMilliseconds(250);
73 
74 // How long each fade should last for.
75 constexpr auto kShowFadeDuration = base::TimeDelta::FromMilliseconds(120);
76 constexpr auto kHideFadeDuration = base::TimeDelta::FromMilliseconds(200);
77 constexpr int kFramerate = 25;
78 
79 // How long each expansion step should take.
80 constexpr auto kMinExpansionStepDuration =
81     base::TimeDelta::FromMilliseconds(20);
82 constexpr auto kMaxExpansionStepDuration =
83     base::TimeDelta::FromMilliseconds(150);
84 
85 // How long to delay before destroying an unused status bubble widget.
86 constexpr auto kDestroyPopupDelay = base::TimeDelta::FromSeconds(10);
87 
GetFont()88 const gfx::FontList& GetFont() {
89   return views::style::GetFont(views::style::CONTEXT_LABEL,
90                                views::style::STYLE_PRIMARY);
91 }
92 
93 }  // namespace
94 
95 // StatusBubbleViews::StatusViewAnimation --------------------------------------
96 class StatusBubbleViews::StatusViewAnimation
97     : public gfx::LinearAnimation,
98       public views::AnimationDelegateViews {
99  public:
100   StatusViewAnimation(StatusView* status_view,
101                       float opacity_start,
102                       float opacity_end);
103   ~StatusViewAnimation() override;
104 
105   float GetCurrentOpacity();
106 
107  private:
108   // gfx::LinearAnimation:
109   void AnimateToState(double state) override;
110 
111   // gfx::AnimationDelegate:
112   void AnimationEnded(const Animation* animation) override;
113 
114   StatusView* status_view_;
115 
116   // Start and end opacities for the current transition - note that as a
117   // fade-in can easily turn into a fade out, opacity_start_ is sometimes
118   // a value between 0 and 1.
119   float opacity_start_;
120   float opacity_end_;
121 
122   DISALLOW_COPY_AND_ASSIGN(StatusViewAnimation);
123 };
124 
125 // StatusBubbleViews::StatusView -----------------------------------------------
126 //
127 // StatusView manages the display of the bubble, applying text changes and
128 // fading in or out the bubble as required.
129 class StatusBubbleViews::StatusView : public views::View {
130  public:
131   // The bubble can be in one of many states:
132   enum BubbleState {
133     BUBBLE_HIDDEN,         // Entirely BUBBLE_HIDDEN.
134     BUBBLE_HIDING_FADE,    // In a fade-out transition.
135     BUBBLE_HIDING_TIMER,   // Waiting before a fade-out.
136     BUBBLE_SHOWING_TIMER,  // Waiting before a fade-in.
137     BUBBLE_SHOWING_FADE,   // In a fade-in transition.
138     BUBBLE_SHOWN           // Fully visible.
139   };
140 
141   enum BubbleStyle {
142     STYLE_BOTTOM,
143     STYLE_FLOATING,
144     STYLE_STANDARD,
145     STYLE_STANDARD_RIGHT
146   };
147 
148   explicit StatusView(StatusBubbleViews* status_bubble);
149   ~StatusView() override;
150 
151   // views::View:
152   gfx::Insets GetInsets() const override;
153 
154   // Set the bubble text, or hide the bubble if |text| is an empty string.
155   // Triggers an animation sequence to display if |should_animate_open| is true.
156   void SetText(const base::string16& text, bool should_animate_open);
157 
state() const158   BubbleState state() const { return state_; }
style() const159   BubbleStyle style() const { return style_; }
160   void SetStyle(BubbleStyle style);
161 
162   // Show the bubble instantly.
163   void ShowInstantly();
164 
165   // Hide the bubble instantly; this may destroy the bubble and view.
166   void HideInstantly();
167 
168   // Resets any timers we have. Typically called when the user moves a mouse.
169   void ResetTimer();
170 
171   // This call backs the StatusView in order to fade the bubble in and out.
172   void SetOpacity(float opacity);
173 
174   // Depending on the state of the bubble this will hide the popup or not.
175   void OnAnimationEnded();
176 
animation()177   gfx::Animation* animation() { return animation_.get(); }
178 
179   bool IsDestroyPopupTimerRunning() const;
180 
181  protected:
182   // views::View:
183   void OnThemeChanged() override;
184 
185  private:
186   class InitialTimer;
187 
188   // Manage the timers that control the delay before a fade begins or ends.
189   void StartTimer(base::TimeDelta time);
190   void OnTimer();
191   void CancelTimer();
192   void RestartTimer(base::TimeDelta delay);
193 
194   // Manage the fades and starting and stopping the animations correctly.
195   void StartFade(float start, float end, base::TimeDelta duration);
196   void StartHiding();
197   void StartShowing();
198 
199   // Set the text label's colors according to the theme.
200   void SetTextLabelColors(views::Label* label);
201 
202   // views::View:
203   const char* GetClassName() const override;
204   void OnPaint(gfx::Canvas* canvas) override;
205 
206   BubbleState state_ = BUBBLE_HIDDEN;
207   BubbleStyle style_ = STYLE_STANDARD;
208 
209   std::unique_ptr<StatusViewAnimation> animation_;
210 
211   // The status bubble that manages the popup widget and this view.
212   StatusBubbleViews* status_bubble_;
213 
214   // The currently-displayed text.
215   views::Label* text_;
216 
217   // A timer used to delay destruction of the popup widget. This is meant to
218   // balance the performance tradeoffs of rapid creation/destruction and the
219   // memory savings of closing the widget when it's hidden and unused.
220   base::OneShotTimer destroy_popup_timer_;
221 
222   base::WeakPtrFactory<StatusBubbleViews::StatusView> timer_factory_{this};
223 
224   DISALLOW_COPY_AND_ASSIGN(StatusView);
225 };
226 
StatusView(StatusBubbleViews * status_bubble)227 StatusBubbleViews::StatusView::StatusView(StatusBubbleViews* status_bubble)
228     : status_bubble_(status_bubble) {
229   animation_ = std::make_unique<StatusViewAnimation>(this, 0, 0);
230 
231   SetLayoutManager(std::make_unique<views::FillLayout>());
232 
233   std::unique_ptr<views::Label> text = std::make_unique<views::Label>();
234   // Don't move this after AddChildView() since this function would trigger
235   // repaint which should not happen in the constructor.
236   SetTextLabelColors(text.get());
237   text->SetHorizontalAlignment(gfx::ALIGN_LEFT);
238   text_ = AddChildView(std::move(text));
239 }
240 
~StatusView()241 StatusBubbleViews::StatusView::~StatusView() {
242   animation_->Stop();
243   CancelTimer();
244 }
245 
GetInsets() const246 gfx::Insets StatusBubbleViews::StatusView::GetInsets() const {
247   return gfx::Insets(kShadowThickness, kShadowThickness + kTextHorizPadding);
248 }
249 
SetText(const base::string16 & text,bool should_animate_open)250 void StatusBubbleViews::StatusView::SetText(const base::string16& text,
251                                             bool should_animate_open) {
252   if (text.empty()) {
253     StartHiding();
254   } else {
255     text_->SetText(text);
256     if (should_animate_open)
257       StartShowing();
258   }
259 }
260 
SetStyle(BubbleStyle style)261 void StatusBubbleViews::StatusView::SetStyle(BubbleStyle style) {
262   if (style_ != style) {
263     style_ = style;
264     SchedulePaint();
265   }
266 }
267 
ShowInstantly()268 void StatusBubbleViews::StatusView::ShowInstantly() {
269   animation_->Stop();
270   CancelTimer();
271   SetOpacity(1.0);
272   state_ = BUBBLE_SHOWN;
273   GetWidget()->ShowInactive();
274   destroy_popup_timer_.Stop();
275 }
276 
HideInstantly()277 void StatusBubbleViews::StatusView::HideInstantly() {
278   animation_->Stop();
279   CancelTimer();
280   SetOpacity(0.0);
281   text_->SetText(base::string16());
282   state_ = BUBBLE_HIDDEN;
283   // Don't orderOut: the window on macOS. Doing so for a child window requires
284   // it to be detached/reattached, which may trigger a space switch. Instead,
285   // just leave the window fully transparent and unclickable.
286   GetWidget()->Hide();
287   destroy_popup_timer_.Stop();
288   // This isn't done in the constructor as tests may change the task runner
289   // after the fact.
290   destroy_popup_timer_.SetTaskRunner(status_bubble_->task_runner_);
291   destroy_popup_timer_.Start(FROM_HERE, kDestroyPopupDelay, status_bubble_,
292                              &StatusBubbleViews::DestroyPopup);
293 }
294 
ResetTimer()295 void StatusBubbleViews::StatusView::ResetTimer() {
296   if (state_ == BUBBLE_SHOWING_TIMER) {
297     // We hadn't yet begun showing anything when we received a new request
298     // for something to show, so we start from scratch.
299     RestartTimer(kShowDelay);
300   }
301 }
302 
SetOpacity(float opacity)303 void StatusBubbleViews::StatusView::SetOpacity(float opacity) {
304   GetWidget()->SetOpacity(opacity);
305 }
306 
OnAnimationEnded()307 void StatusBubbleViews::StatusView::OnAnimationEnded() {
308   if (state_ == BUBBLE_SHOWING_FADE)
309     state_ = BUBBLE_SHOWN;
310   else if (state_ == BUBBLE_HIDING_FADE)
311     HideInstantly();  // This view may be destroyed after calling HideInstantly.
312 }
313 
IsDestroyPopupTimerRunning() const314 bool StatusBubbleViews::StatusView::IsDestroyPopupTimerRunning() const {
315   return destroy_popup_timer_.IsRunning();
316 }
317 
OnThemeChanged()318 void StatusBubbleViews::StatusView::OnThemeChanged() {
319   views::View::OnThemeChanged();
320   SetTextLabelColors(text_);
321 }
322 
StartTimer(base::TimeDelta time)323 void StatusBubbleViews::StatusView::StartTimer(base::TimeDelta time) {
324   if (timer_factory_.HasWeakPtrs())
325     timer_factory_.InvalidateWeakPtrs();
326 
327   status_bubble_->task_runner_->PostDelayedTask(
328       FROM_HERE,
329       base::BindOnce(&StatusBubbleViews::StatusView::OnTimer,
330                      timer_factory_.GetWeakPtr()),
331       time);
332 }
333 
OnTimer()334 void StatusBubbleViews::StatusView::OnTimer() {
335   if (state_ == BUBBLE_HIDING_TIMER) {
336     state_ = BUBBLE_HIDING_FADE;
337     StartFade(1.0f, 0.0f, kHideFadeDuration);
338   } else if (state_ == BUBBLE_SHOWING_TIMER) {
339     state_ = BUBBLE_SHOWING_FADE;
340     StartFade(0.0f, 1.0f, kShowFadeDuration);
341   }
342 }
343 
CancelTimer()344 void StatusBubbleViews::StatusView::CancelTimer() {
345   if (timer_factory_.HasWeakPtrs())
346     timer_factory_.InvalidateWeakPtrs();
347 }
348 
RestartTimer(base::TimeDelta delay)349 void StatusBubbleViews::StatusView::RestartTimer(base::TimeDelta delay) {
350   CancelTimer();
351   StartTimer(delay);
352 }
353 
StartFade(float start,float end,base::TimeDelta duration)354 void StatusBubbleViews::StatusView::StartFade(float start,
355                                               float end,
356                                               base::TimeDelta duration) {
357   animation_ = std::make_unique<StatusViewAnimation>(this, start, end);
358 
359   // This will also reset the currently-occurring animation.
360   animation_->SetDuration(duration);
361   animation_->Start();
362 }
363 
StartHiding()364 void StatusBubbleViews::StatusView::StartHiding() {
365   if (state_ == BUBBLE_SHOWN) {
366     state_ = BUBBLE_HIDING_TIMER;
367     StartTimer(kHideDelay);
368   } else if (state_ == BUBBLE_SHOWING_FADE) {
369     state_ = BUBBLE_HIDING_FADE;
370     // Figure out where we are in the current fade.
371     float current_opacity = animation_->GetCurrentOpacity();
372 
373     // Start a fade in the opposite direction.
374     StartFade(current_opacity, 0.0f, kHideFadeDuration * current_opacity);
375   } else if (state_ == BUBBLE_SHOWING_TIMER) {
376     HideInstantly();  // This view may be destroyed after calling HideInstantly.
377   }
378 }
379 
StartShowing()380 void StatusBubbleViews::StatusView::StartShowing() {
381   destroy_popup_timer_.Stop();
382 
383   if (state_ == BUBBLE_HIDDEN) {
384     GetWidget()->ShowInactive();
385     state_ = BUBBLE_SHOWING_TIMER;
386     StartTimer(kShowDelay);
387   } else if (state_ == BUBBLE_HIDING_TIMER) {
388     state_ = BUBBLE_SHOWN;
389     CancelTimer();
390   } else if (state_ == BUBBLE_HIDING_FADE) {
391     // We're partway through a fade.
392     state_ = BUBBLE_SHOWING_FADE;
393 
394     // Figure out where we are in the current fade.
395     float current_opacity = animation_->GetCurrentOpacity();
396 
397     // Start a fade in the opposite direction.
398     StartFade(current_opacity, 1.0f, kShowFadeDuration * current_opacity);
399   } else if (state_ == BUBBLE_SHOWING_TIMER) {
400     // We hadn't yet begun showing anything when we received a new request
401     // for something to show, so we start from scratch.
402     ResetTimer();
403   }
404 }
405 
SetTextLabelColors(views::Label * text)406 void StatusBubbleViews::StatusView::SetTextLabelColors(views::Label* text) {
407   const auto* theme_provider = status_bubble_->base_view()->GetThemeProvider();
408   SkColor bubble_color =
409       theme_provider->GetColor(ThemeProperties::COLOR_STATUS_BUBBLE);
410   text->SetBackgroundColor(bubble_color);
411   // Text color is the background tab text color, adjusted if required.
412   text->SetEnabledColor(theme_provider->GetColor(
413       ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE));
414 }
415 
GetClassName() const416 const char* StatusBubbleViews::StatusView::GetClassName() const {
417   return "StatusBubbleViews::StatusView";
418 }
419 
OnPaint(gfx::Canvas * canvas)420 void StatusBubbleViews::StatusView::OnPaint(gfx::Canvas* canvas) {
421   gfx::ScopedCanvas scoped(canvas);
422   float scale = canvas->UndoDeviceScaleFactor();
423   const float radius = kBubbleCornerRadius * scale;
424 
425   SkScalar rad[8] = {};
426 
427   // Top Edges - if the bubble is in its bottom position (sticking downwards),
428   // then we square the top edges. Otherwise, we square the edges based on the
429   // position of the bubble within the window (the bubble is positioned in the
430   // southeast corner in RTL and in the southwest corner in LTR).
431   if (style_ != STYLE_BOTTOM) {
432     if (base::i18n::IsRTL() != (style_ == STYLE_STANDARD_RIGHT)) {
433       // The text is RtL or the bubble is on the right side (but not both).
434 
435       // Top Left corner.
436       rad[0] = radius;
437       rad[1] = radius;
438     } else {
439       // Top Right corner.
440       rad[2] = radius;
441       rad[3] = radius;
442     }
443   }
444 
445   // Bottom edges - Keep these squared off if the bubble is in its standard
446   // position (sticking upward).
447   if (style_ != STYLE_STANDARD && style_ != STYLE_STANDARD_RIGHT) {
448     // Bottom Right Corner.
449     rad[4] = radius;
450     rad[5] = radius;
451 
452     // Bottom Left Corner.
453     rad[6] = radius;
454     rad[7] = radius;
455   }
456 
457   // Snap to pixels to avoid shadow blurriness.
458   gfx::Size scaled_size = gfx::ScaleToRoundedSize(size(), scale);
459 
460   // This needs to be pixel-aligned too. Floor is perferred here because a more
461   // conservative value prevents the bottom edge from occasionally leaving a gap
462   // where the web content is visible.
463   const int shadow_thickness_pixels = std::floor(kShadowThickness * scale);
464 
465   // The shadow will overlap the window frame. Clip it off when the bubble is
466   // docked. Otherwise when the bubble is floating preserve the full shadow so
467   // the bubble looks complete.
468   int clip_left = style_ == STYLE_STANDARD ? shadow_thickness_pixels : 0;
469   int clip_right = style_ == STYLE_STANDARD_RIGHT ? shadow_thickness_pixels : 0;
470   if (base::i18n::IsRTL())
471     std::swap(clip_left, clip_right);
472 
473   const int clip_bottom = clip_left || clip_right ? shadow_thickness_pixels : 0;
474   gfx::Rect clip_rect(scaled_size);
475   clip_rect.Inset(clip_left, 0, clip_right, clip_bottom);
476   canvas->ClipRect(clip_rect);
477 
478   gfx::RectF bubble_rect{gfx::SizeF(scaled_size)};
479   // Reposition() moves the bubble down and to the left in order to overlap the
480   // client edge (or window frame when there's no client edge) by 1 DIP. We want
481   // a 1 pixel shadow on the innermost pixel of that overlap. So we inset the
482   // bubble bounds by 1 DIP minus 1 pixel. Failing to do this results in drawing
483   // further and further outside the window as the scale increases.
484   const int inset = shadow_thickness_pixels - 1;
485   bubble_rect.Inset(style_ == STYLE_STANDARD_RIGHT ? 0 : inset, 0,
486                     style_ == STYLE_STANDARD_RIGHT ? inset : 0, inset);
487   // Align to pixel centers now that the layout is correct.
488   bubble_rect.Inset(0.5, 0.5);
489 
490   SkPath path;
491   path.addRoundRect(gfx::RectFToSkRect(bubble_rect), rad);
492 
493   cc::PaintFlags flags;
494   flags.setStyle(cc::PaintFlags::kStroke_Style);
495   flags.setStrokeWidth(1);
496   flags.setAntiAlias(true);
497 
498   SkPath stroke_path;
499   flags.getFillPath(path, &stroke_path);
500 
501   // Get the fill path by subtracting the shadow so they align neatly.
502   SkPath fill_path;
503   Op(path, stroke_path, kDifference_SkPathOp, &fill_path);
504   flags.setStyle(cc::PaintFlags::kFill_Style);
505 
506   const auto* theme_provider = status_bubble_->base_view()->GetThemeProvider();
507   const SkColor bubble_color =
508       theme_provider->GetColor(ThemeProperties::COLOR_STATUS_BUBBLE);
509   flags.setColor(bubble_color);
510   canvas->sk_canvas()->drawPath(fill_path, flags);
511 
512   flags.setColor(kShadowColor);
513   canvas->sk_canvas()->drawPath(stroke_path, flags);
514 }
515 
516 
517 // StatusBubbleViews::StatusViewAnimation --------------------------------------
518 
StatusViewAnimation(StatusView * status_view,float opacity_start,float opacity_end)519 StatusBubbleViews::StatusViewAnimation::StatusViewAnimation(
520     StatusView* status_view,
521     float opacity_start,
522     float opacity_end)
523     : gfx::LinearAnimation(this, kFramerate),
524       views::AnimationDelegateViews(status_view),
525       status_view_(status_view),
526       opacity_start_(opacity_start),
527       opacity_end_(opacity_end) {}
528 
~StatusViewAnimation()529 StatusBubbleViews::StatusViewAnimation::~StatusViewAnimation() {
530   // Remove ourself as a delegate so that we don't get notified when
531   // animations end as a result of destruction.
532   set_delegate(NULL);
533 }
534 
GetCurrentOpacity()535 float StatusBubbleViews::StatusViewAnimation::GetCurrentOpacity() {
536   return static_cast<float>(opacity_start_ +
537                             (opacity_end_ - opacity_start_) *
538                                 gfx::LinearAnimation::GetCurrentValue());
539 }
540 
AnimateToState(double state)541 void StatusBubbleViews::StatusViewAnimation::AnimateToState(double state) {
542   status_view_->SetOpacity(GetCurrentOpacity());
543 }
544 
AnimationEnded(const gfx::Animation * animation)545 void StatusBubbleViews::StatusViewAnimation::AnimationEnded(
546     const gfx::Animation* animation) {
547   status_view_->SetOpacity(opacity_end_);
548   status_view_->OnAnimationEnded();
549 }
550 
551 // StatusBubbleViews::StatusViewExpander ---------------------------------------
552 //
553 // Manages the expansion and contraction of the status bubble as it accommodates
554 // URLs too long to fit in the standard bubble. Changes are passed through the
555 // StatusView to paint.
556 class StatusBubbleViews::StatusViewExpander
557     : public gfx::LinearAnimation,
558       public views::AnimationDelegateViews {
559  public:
StatusViewExpander(StatusBubbleViews * status_bubble,StatusView * status_view)560   StatusViewExpander(StatusBubbleViews* status_bubble, StatusView* status_view)
561       : gfx::LinearAnimation(this, kFramerate),
562         views::AnimationDelegateViews(status_view),
563         status_bubble_(status_bubble),
564         status_view_(status_view) {}
565 
566   // Manage the expansion of the bubble.
567   void StartExpansion(const base::string16& expanded_text,
568                       int current_width,
569                       int expansion_end);
570 
571  private:
572   // Animation functions.
573   int GetCurrentBubbleWidth();
574   void SetBubbleWidth(int width);
575   void AnimateToState(double state) override;
576   void AnimationEnded(const gfx::Animation* animation) override;
577 
578   // The status bubble that manages the popup widget and this object.
579   StatusBubbleViews* status_bubble_;
580 
581   // Change the bounds and text of this view.
582   StatusView* status_view_;
583 
584   // Text elided (if needed) to fit maximum status bar width.
585   base::string16 expanded_text_;
586 
587   // Widths at expansion start and end.
588   int expansion_start_ = 0;
589   int expansion_end_ = 0;
590 
591   DISALLOW_COPY_AND_ASSIGN(StatusViewExpander);
592 };
593 
AnimateToState(double state)594 void StatusBubbleViews::StatusViewExpander::AnimateToState(double state) {
595   SetBubbleWidth(GetCurrentBubbleWidth());
596 }
597 
AnimationEnded(const gfx::Animation * animation)598 void StatusBubbleViews::StatusViewExpander::AnimationEnded(
599     const gfx::Animation* animation) {
600   status_view_->SetText(expanded_text_, false);
601   SetBubbleWidth(expansion_end_);
602   // WARNING: crash data seems to indicate |this| may be deleted by the time
603   // SetBubbleWidth() returns.
604 }
605 
StartExpansion(const base::string16 & expanded_text,int expansion_start,int expansion_end)606 void StatusBubbleViews::StatusViewExpander::StartExpansion(
607     const base::string16& expanded_text,
608     int expansion_start,
609     int expansion_end) {
610   expanded_text_ = expanded_text;
611   expansion_start_ = expansion_start;
612   expansion_end_ = expansion_end;
613   base::TimeDelta min_duration = std::max(
614       kMinExpansionStepDuration,
615       kMaxExpansionStepDuration * (expansion_end - expansion_start) / 100.0);
616   SetDuration(std::min(kMaxExpansionStepDuration, min_duration));
617   Start();
618 }
619 
GetCurrentBubbleWidth()620 int StatusBubbleViews::StatusViewExpander::GetCurrentBubbleWidth() {
621   return static_cast<int>(expansion_start_ +
622       (expansion_end_ - expansion_start_) *
623           gfx::LinearAnimation::GetCurrentValue());
624 }
625 
SetBubbleWidth(int width)626 void StatusBubbleViews::StatusViewExpander::SetBubbleWidth(int width) {
627   status_view_->SchedulePaint();
628   status_bubble_->SetBubbleWidth(width);
629   // WARNING: crash data seems to indicate |this| may be deleted by the time
630   // SetBubbleWidth() returns.
631 }
632 
633 
634 // StatusBubbleViews -----------------------------------------------------------
635 
636 const int StatusBubbleViews::kShadowThickness = 1;
637 
StatusBubbleViews(views::View * base_view)638 StatusBubbleViews::StatusBubbleViews(views::View* base_view)
639     : base_view_(base_view),
640       task_runner_(base::ThreadTaskRunnerHandle::Get().get()) {}
641 
~StatusBubbleViews()642 StatusBubbleViews::~StatusBubbleViews() {
643   DestroyPopup();
644 }
645 
InitPopup()646 void StatusBubbleViews::InitPopup() {
647   if (!popup_) {
648     DCHECK(!view_);
649     DCHECK(!expand_view_);
650     popup_ = std::make_unique<views::Widget>();
651 
652     views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
653 #if defined(OS_WIN)
654     // On Windows use the software compositor to ensure that we don't block
655     // the UI thread blocking issue during command buffer creation. We can
656     // revert this change once http://crbug.com/125248 is fixed.
657     params.force_software_compositing = true;
658 #endif
659     params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
660     params.accept_events = false;
661     params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
662     views::Widget* frame = base_view_->GetWidget();
663     params.parent = frame->GetNativeView();
664     params.context = frame->GetNativeWindow();
665     params.name = "StatusBubble";
666 #if defined(OS_CHROMEOS)
667     params.init_properties_container.SetProperty(ash::kHideInOverviewKey, true);
668     params.init_properties_container.SetProperty(ash::kHideInDeskMiniViewKey,
669                                                  true);
670 #endif  // defined(OS_CHROMEOS)
671     popup_->Init(std::move(params));
672     // We do our own animation and don't want any from the system.
673     popup_->SetVisibilityChangedAnimationsEnabled(false);
674     popup_->SetOpacity(0.f);
675     view_ = popup_->SetContentsView(std::make_unique<StatusView>(this));
676     expand_view_ = std::make_unique<StatusViewExpander>(this, view_);
677 #if !defined(OS_MAC)
678     // Stack the popup above the base widget and below higher z-order windows.
679     // This is unnecessary and even detrimental on Mac, see CreateBubbleWidget.
680     popup_->StackAboveWidget(frame);
681 #endif
682     RepositionPopup();
683   }
684 }
685 
DestroyPopup()686 void StatusBubbleViews::DestroyPopup() {
687   CancelExpandTimer();
688   expand_view_.reset();
689   view_ = nullptr;
690   // Move |popup_| to the stack to avoid reentrancy issues with CloseNow().
691   if (std::unique_ptr<views::Widget> popup = std::move(popup_))
692     popup->CloseNow();
693 }
694 
Reposition()695 void StatusBubbleViews::Reposition() {
696   // Overlap the client edge that's shown in restored mode, or when there is no
697   // client edge this makes the bubble snug with the corner of the window.
698   int overlap = kShadowThickness;
699   int height = GetPreferredHeight();
700   int base_view_height = base_view_->bounds().height();
701   gfx::Point origin(-overlap, base_view_height - height + overlap);
702   SetBounds(origin.x(), origin.y(), base_view_->bounds().width() / 3, height);
703 }
704 
RepositionPopup()705 void StatusBubbleViews::RepositionPopup() {
706   if (popup_.get()) {
707     gfx::Point top_left;
708     // TODO(flackr): Get the non-transformed point so that the status bubble
709     // popup window's position is consistent with the base_view_'s window.
710     views::View::ConvertPointToScreen(base_view_, &top_left);
711     popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(),
712                                 top_left.y() + position_.y(),
713                                 size_.width(), size_.height()));
714   }
715 }
716 
GetPreferredHeight()717 int StatusBubbleViews::GetPreferredHeight() {
718   return GetFont().GetHeight() + kTotalVerticalPadding;
719 }
720 
SetBounds(int x,int y,int w,int h)721 void StatusBubbleViews::SetBounds(int x, int y, int w, int h) {
722   original_position_.SetPoint(x, y);
723   position_.SetPoint(base_view_->GetMirroredXWithWidthInView(x, w), y);
724   size_.SetSize(w, h);
725   RepositionPopup();
726   if (popup_.get() && contains_mouse_)
727     AvoidMouse(last_mouse_moved_location_);
728 }
729 
GetWidthForURL(const base::string16 & url_string)730 int StatusBubbleViews::GetWidthForURL(const base::string16& url_string) {
731   // Get the width of the elided url
732   int elided_url_width = gfx::GetStringWidth(url_string, GetFont());
733   // Add proper paddings
734   return elided_url_width + (kShadowThickness + kTextHorizPadding) * 2 + 1;
735 }
736 
OnThemeChanged()737 void StatusBubbleViews::OnThemeChanged() {
738   if (popup_)
739     popup_->ThemeChanged();
740 }
741 
SetStatus(const base::string16 & status_text)742 void StatusBubbleViews::SetStatus(const base::string16& status_text) {
743   if (size_.IsEmpty())
744     return;  // We have no bounds, don't attempt to show the popup.
745 
746   if (status_text_ == status_text && !status_text.empty())
747     return;
748 
749   if (!IsFrameVisible())
750     return;  // Don't show anything if the parent isn't visible.
751 
752   status_text_ = status_text;
753   if (status_text_.empty() && url_text_.empty() && !popup_)
754     return;
755 
756   InitPopup();
757   view_->SetText(!status_text_.empty() ? status_text_ : url_text_, true);
758   if (!status_text_.empty()) {
759     SetBubbleWidth(GetStandardStatusBubbleWidth());
760     view_->ShowInstantly();
761   }
762 }
763 
SetURL(const GURL & url)764 void StatusBubbleViews::SetURL(const GURL& url) {
765   url_ = url;
766   if (size_.IsEmpty())
767     return;  // We have no bounds, don't attempt to show the popup.
768 
769   if (url.is_empty() && status_text_.empty() && !popup_)
770     return;
771 
772   InitPopup();
773 
774   // If we want to clear a displayed URL but there is a status still to
775   // display, display that status instead.
776   if (url.is_empty() && !status_text_.empty()) {
777     url_text_ = base::string16();
778     if (IsFrameVisible())
779       view_->SetText(status_text_, true);
780     return;
781   }
782 
783   // Set Elided Text corresponding to the GURL object.
784   int text_width = static_cast<int>(
785       size_.width() - (kShadowThickness + kTextHorizPadding) * 2 - 1);
786   url_text_ = url_formatter::ElideUrl(url, GetFont(), text_width);
787 
788   // Get the width of the URL if the bubble width is the maximum size.
789   base::string16 full_size_elided_url =
790       url_formatter::ElideUrl(url, GetFont(), GetMaxStatusBubbleWidth());
791   int url_width = GetWidthForURL(full_size_elided_url);
792 
793   // Get the width for the url if it is unexpanded.
794   int unexpanded_width = std::min(url_width, GetStandardStatusBubbleWidth());
795 
796   // Reset expansion state only when bubble is completely hidden.
797   if (view_->state() == StatusView::BUBBLE_HIDDEN) {
798     is_expanded_ = false;
799     url_text_ = url_formatter::ElideUrl(url, GetFont(), unexpanded_width);
800     SetBubbleWidth(unexpanded_width);
801   }
802 
803   if (IsFrameVisible()) {
804     // If bubble is not expanded & not empty, make it fit properly in the
805     // unexpanded bubble
806     if (!is_expanded_ & !url.is_empty()) {
807       url_text_ = url_formatter::ElideUrl(url, GetFont(), unexpanded_width);
808       SetBubbleWidth(unexpanded_width);
809     }
810 
811     CancelExpandTimer();
812 
813     // If bubble is already in expanded state, shift to adjust to new text
814     // size (shrinking or expanding). Otherwise delay.
815     if (is_expanded_ && !url.is_empty()) {
816       ExpandBubble();
817     } else if (url_formatter::FormatUrl(url).length() >
818                url_text_.length()) {
819       task_runner_->PostDelayedTask(
820           FROM_HERE,
821           base::BindOnce(&StatusBubbleViews::ExpandBubble,
822                          expand_timer_factory_.GetWeakPtr()),
823           base::TimeDelta::FromMilliseconds(kExpandHoverDelayMS));
824     }
825     // An URL is always treated as a left-to-right string. On right-to-left UIs
826     // we need to explicitly mark the URL as LTR to make sure it is displayed
827     // correctly.
828     view_->SetText(base::i18n::GetDisplayStringInLTRDirectionality(url_text_),
829                    true);
830   }
831 }
832 
Hide()833 void StatusBubbleViews::Hide() {
834   status_text_ = base::string16();
835   url_text_ = base::string16();
836   if (view_)
837     view_->HideInstantly();
838 }
839 
MouseMoved(bool left_content)840 void StatusBubbleViews::MouseMoved(bool left_content) {
841   MouseMovedAt(display::Screen::GetScreen()->GetCursorScreenPoint(),
842                left_content);
843 }
844 
MouseMovedAt(const gfx::Point & location,bool left_content)845 void StatusBubbleViews::MouseMovedAt(const gfx::Point& location,
846                                      bool left_content) {
847   contains_mouse_ = !left_content;
848   if (left_content) {
849     RepositionPopup();
850     return;
851   }
852   last_mouse_moved_location_ = location;
853 
854   if (view_) {
855     view_->ResetTimer();
856 
857     if (view_->state() != StatusView::BUBBLE_HIDDEN &&
858         view_->state() != StatusView::BUBBLE_HIDING_FADE &&
859         view_->state() != StatusView::BUBBLE_HIDING_TIMER) {
860       AvoidMouse(location);
861     }
862   }
863 }
864 
UpdateDownloadShelfVisibility(bool visible)865 void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) {
866   download_shelf_is_visible_ = visible;
867 }
868 
AvoidMouse(const gfx::Point & location)869 void StatusBubbleViews::AvoidMouse(const gfx::Point& location) {
870   DCHECK(view_);
871   // Get the position of the frame.
872   gfx::Point top_left;
873   views::View::ConvertPointToScreen(base_view_, &top_left);
874   // Border included.
875   int window_width = base_view_->GetLocalBounds().width();
876 
877   // Get the cursor position relative to the popup.
878   gfx::Point relative_location = location;
879   if (base::i18n::IsRTL()) {
880     int top_right_x = top_left.x() + window_width;
881     relative_location.set_x(top_right_x - relative_location.x());
882   } else {
883     relative_location.set_x(
884         relative_location.x() - (top_left.x() + position_.x()));
885   }
886   relative_location.set_y(
887       relative_location.y() - (top_left.y() + position_.y()));
888 
889   // If the mouse is in a position where we think it would move the
890   // status bubble, figure out where and how the bubble should be moved.
891   if (relative_location.y() > -kMousePadding &&
892       relative_location.x() < size_.width() + kMousePadding) {
893     int offset = kMousePadding + relative_location.y();
894 
895     // Make the movement non-linear.
896     offset = offset * offset / kMousePadding;
897 
898     // When the mouse is entering from the right, we want the offset to be
899     // scaled by how horizontally far away the cursor is from the bubble.
900     if (relative_location.x() > size_.width()) {
901       offset = static_cast<int>(static_cast<float>(offset) * (
902           static_cast<float>(kMousePadding -
903               (relative_location.x() - size_.width())) /
904           static_cast<float>(kMousePadding)));
905     }
906 
907     // Cap the offset and change the visual presentation of the bubble
908     // depending on where it ends up (so that rounded corners square off
909     // and mate to the edges of the tab content).
910     if (offset >= size_.height() - kShadowThickness * 2) {
911       offset = size_.height() - kShadowThickness * 2;
912       view_->SetStyle(StatusView::STYLE_BOTTOM);
913     } else if (offset > kBubbleCornerRadius / 2 - kShadowThickness) {
914       view_->SetStyle(StatusView::STYLE_FLOATING);
915     } else {
916       view_->SetStyle(StatusView::STYLE_STANDARD);
917     }
918 
919     // Check if the bubble sticks out from the monitor or will obscure
920     // download shelf.
921     gfx::NativeView view = base_view_->GetWidget()->GetNativeView();
922     gfx::Rect monitor_rect =
923         display::Screen::GetScreen()->GetDisplayNearestView(view).work_area();
924     const int bubble_bottom_y = top_left.y() + position_.y() + size_.height();
925 
926     if (bubble_bottom_y + offset > monitor_rect.height() ||
927         (download_shelf_is_visible_ &&
928          (view_->style() == StatusView::STYLE_FLOATING ||
929           view_->style() == StatusView::STYLE_BOTTOM))) {
930       // The offset is still too large. Move the bubble to the right and reset
931       // Y offset_ to zero.
932       view_->SetStyle(StatusView::STYLE_STANDARD_RIGHT);
933       offset_ = 0;
934 
935       // Subtract border width + bubble width.
936       int right_position_x = window_width - (position_.x() + size_.width());
937       popup_->SetBounds(gfx::Rect(top_left.x() + right_position_x,
938                                   top_left.y() + position_.y(),
939                                   size_.width(), size_.height()));
940     } else {
941       offset_ = offset;
942       popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(),
943                                   top_left.y() + position_.y() + offset_,
944                                   size_.width(), size_.height()));
945     }
946   } else if (offset_ != 0 ||
947       view_->style() == StatusView::STYLE_STANDARD_RIGHT) {
948     offset_ = 0;
949     view_->SetStyle(StatusView::STYLE_STANDARD);
950     popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(),
951                                 top_left.y() + position_.y(),
952                                 size_.width(), size_.height()));
953   }
954 }
955 
IsFrameVisible()956 bool StatusBubbleViews::IsFrameVisible() {
957   views::Widget* frame = base_view_->GetWidget();
958   if (!frame->IsVisible())
959     return false;
960 
961   views::Widget* window = frame->GetTopLevelWidget();
962   return !window || !window->IsMinimized();
963 }
964 
IsFrameMaximized()965 bool StatusBubbleViews::IsFrameMaximized() {
966   views::Widget* frame = base_view_->GetWidget();
967   views::Widget* window = frame->GetTopLevelWidget();
968   return window && window->IsMaximized();
969 }
970 
ExpandBubble()971 void StatusBubbleViews::ExpandBubble() {
972   // Elide URL to maximum possible size, then check actual length (it may
973   // still be too long to fit) before expanding bubble.
974   url_text_ =
975       url_formatter::ElideUrl(url_, GetFont(), GetMaxStatusBubbleWidth());
976   int expanded_bubble_width =
977       std::min(GetWidthForURL(url_text_), GetMaxStatusBubbleWidth());
978   is_expanded_ = true;
979   expand_view_->StartExpansion(url_text_, size_.width(), expanded_bubble_width);
980 }
981 
GetStandardStatusBubbleWidth()982 int StatusBubbleViews::GetStandardStatusBubbleWidth() {
983   return base_view_->bounds().width() / 3;
984 }
985 
GetMaxStatusBubbleWidth()986 int StatusBubbleViews::GetMaxStatusBubbleWidth() {
987   const ui::NativeTheme* theme = base_view_->GetNativeTheme();
988   return static_cast<int>(
989       std::max(0, base_view_->bounds().width() -
990                       (kShadowThickness + kTextHorizPadding) * 2 - 1 -
991                       views::ScrollBarViews::GetVerticalScrollBarWidth(theme)));
992 }
993 
SetBubbleWidth(int width)994 void StatusBubbleViews::SetBubbleWidth(int width) {
995   size_.set_width(width);
996   SetBounds(original_position_.x(), original_position_.y(),
997             size_.width(), size_.height());
998 }
999 
CancelExpandTimer()1000 void StatusBubbleViews::CancelExpandTimer() {
1001   if (expand_timer_factory_.HasWeakPtrs())
1002     expand_timer_factory_.InvalidateWeakPtrs();
1003 }
1004 
GetShowHideAnimationForTest()1005 gfx::Animation* StatusBubbleViews::GetShowHideAnimationForTest() {
1006   return view_ ? view_->animation() : nullptr;
1007 }
1008 
IsDestroyPopupTimerRunningForTest()1009 bool StatusBubbleViews::IsDestroyPopupTimerRunningForTest() {
1010   return view_ && view_->IsDestroyPopupTimerRunning();
1011 }
1012