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