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