Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / status_bubble_mac.mm
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/cocoa/status_bubble_mac.h"
6
7 #include <limits>
8
9 #include "base/bind.h"
10 #include "base/compiler_specific.h"
11 #include "base/mac/mac_util.h"
12 #include "base/mac/scoped_block.h"
13 #include "base/mac/sdk_forward_declarations.h"
14 #include "base/message_loop/message_loop.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/sys_string_conversions.h"
17 #include "base/strings/utf_string_conversions.h"
18 #import "chrome/browser/ui/cocoa/bubble_view.h"
19 #include "chrome/browser/ui/elide_url.h"
20 #include "net/base/net_util.h"
21 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
22 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
23 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
24 #include "ui/base/cocoa/window_size_constants.h"
25 #include "ui/gfx/font_list.h"
26 #include "ui/gfx/point.h"
27 #include "ui/gfx/text_elider.h"
28 #include "ui/gfx/text_utils.h"
29
30 namespace {
31
32 const int kWindowHeight = 18;
33
34 // The width of the bubble in relation to the width of the parent window.
35 const CGFloat kWindowWidthPercent = 1.0 / 3.0;
36
37 // How close the mouse can get to the infobubble before it starts sliding
38 // off-screen.
39 const int kMousePadding = 20;
40
41 const int kTextPadding = 3;
42
43 // The status bubble's maximum opacity, when fully faded in.
44 const CGFloat kBubbleOpacity = 1.0;
45
46 // Delay before showing or hiding the bubble after a SetStatus or SetURL call.
47 const int64 kShowDelayMS = 80;
48 const int64 kHideDelayMS = 250;
49
50 // How long each fade should last.
51 const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
52 const NSTimeInterval kHideFadeOutDurationSeconds = 0.200;
53
54 // The minimum representable time interval.  This can be used as the value
55 // passed to +[NSAnimationContext setDuration:] to stop an in-progress
56 // animation as quickly as possible.
57 const NSTimeInterval kMinimumTimeInterval =
58     std::numeric_limits<NSTimeInterval>::min();
59
60 // How quickly the status bubble should expand.
61 const CGFloat kExpansionDurationSeconds = 0.125;
62
63 }  // namespace
64
65 @interface StatusBubbleAnimationDelegate : NSObject {
66  @private
67   base::mac::ScopedBlock<void (^)(void)> completionHandler_;
68 }
69
70 - (id)initWithCompletionHandler:(void (^)(void))completionHandler;
71
72 // CAAnimation delegate method
73 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
74 @end
75
76 @implementation StatusBubbleAnimationDelegate
77
78 - (id)initWithCompletionHandler:(void (^)(void))completionHandler {
79   if ((self = [super init])) {
80     completionHandler_.reset(completionHandler, base::scoped_policy::RETAIN);
81   }
82
83   return self;
84 }
85
86 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
87   completionHandler_.get()();
88 }
89
90 @end
91
92 @interface StatusBubbleWindow : NSWindow {
93  @private
94   void (^completionHandler_)(void);
95 }
96
97 - (id)animationForKey:(NSString *)key;
98 - (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
99         completionHandler:(void (^)(void))completionHandler;
100 @end
101
102 @implementation StatusBubbleWindow
103
104 - (id)animationForKey:(NSString *)key {
105   CAAnimation* animation = [super animationForKey:key];
106   // If completionHandler_ isn't nil, then this is the first of (potentially)
107   // multiple animations in a grouping; give it the completion handler. If
108   // completionHandler_ is nil, then some other animation was tagged with the
109   // completion handler.
110   if (completionHandler_) {
111     DCHECK(![NSAnimationContext respondsToSelector:
112                @selector(runAnimationGroup:completionHandler:)]);
113     StatusBubbleAnimationDelegate* animation_delegate =
114         [[StatusBubbleAnimationDelegate alloc]
115              initWithCompletionHandler:completionHandler_];
116     [animation setDelegate:animation_delegate];
117     completionHandler_ = nil;
118   }
119   return animation;
120 }
121
122 - (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
123         completionHandler:(void (^)(void))completionHandler {
124   if ([NSAnimationContext respondsToSelector:
125           @selector(runAnimationGroup:completionHandler:)]) {
126     [NSAnimationContext runAnimationGroup:changes
127                         completionHandler:completionHandler];
128   } else {
129     // Mac OS 10.6 does not have completion handler callbacks at the Cocoa
130     // level, only at the CoreAnimation level. So intercept calls made to
131     // -animationForKey: and tag one of the animations with a delegate that will
132     // execute the completion handler.
133     completionHandler_ = completionHandler;
134     [NSAnimationContext beginGrouping];
135     changes([NSAnimationContext currentContext]);
136     // At this point, -animationForKey should have been called by CoreAnimation
137     // to set up the animation to run. Verify this.
138     DCHECK(completionHandler_ == nil);
139     [NSAnimationContext endGrouping];
140   }
141 }
142
143 @end
144
145 StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
146     : timer_factory_(this),
147       expand_timer_factory_(this),
148       completion_handler_factory_(this),
149       parent_(parent),
150       delegate_(delegate),
151       window_(nil),
152       status_text_(nil),
153       url_text_(nil),
154       state_(kBubbleHidden),
155       immediate_(false),
156       is_expanded_(false) {
157   Create();
158   Attach();
159 }
160
161 StatusBubbleMac::~StatusBubbleMac() {
162   DCHECK(window_);
163
164   Hide();
165
166   completion_handler_factory_.InvalidateWeakPtrs();
167   Detach();
168   [window_ release];
169   window_ = nil;
170 }
171
172 void StatusBubbleMac::SetStatus(const base::string16& status) {
173   SetText(status, false);
174 }
175
176 void StatusBubbleMac::SetURL(const GURL& url, const std::string& languages) {
177   url_ = url;
178   languages_ = languages;
179
180   NSRect frame = [window_ frame];
181
182   // Reset frame size when bubble is hidden.
183   if (state_ == kBubbleHidden) {
184     is_expanded_ = false;
185     frame.size.width = NSWidth(CalculateWindowFrame(/*expand=*/false));
186     [window_ setFrame:frame display:NO];
187   }
188
189   int text_width = static_cast<int>(NSWidth(frame) -
190                                     kBubbleViewTextPositionX -
191                                     kTextPadding);
192
193   // Scale from view to window coordinates before eliding URL string.
194   NSSize scaled_width = NSMakeSize(text_width, 0);
195   scaled_width = [[parent_ contentView] convertSize:scaled_width fromView:nil];
196   text_width = static_cast<int>(scaled_width.width);
197   NSFont* font = [[window_ contentView] font];
198   gfx::FontList font_list_chr(
199       gfx::Font(base::SysNSStringToUTF8([font fontName]), [font pointSize]));
200
201   base::string16 original_url_text = net::FormatUrl(url, languages);
202   base::string16 status =
203       ElideUrl(url, font_list_chr, text_width, languages);
204
205   SetText(status, true);
206
207   // In testing, don't use animation. When ExpandBubble is tested, it is
208   // called explicitly.
209   if (immediate_)
210     return;
211   else
212     CancelExpandTimer();
213
214   // If the bubble has been expanded, the user has already hovered over a link
215   // to trigger the expanded state.  Don't wait to change the bubble in this
216   // case -- immediately expand or contract to fit the URL.
217   if (is_expanded_ && !url.is_empty()) {
218     ExpandBubble();
219   } else if (original_url_text.length() > status.length()) {
220     base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
221         base::Bind(&StatusBubbleMac::ExpandBubble,
222                    expand_timer_factory_.GetWeakPtr()),
223         base::TimeDelta::FromMilliseconds(kExpandHoverDelayMS));
224   }
225 }
226
227 void StatusBubbleMac::SetText(const base::string16& text, bool is_url) {
228   // The status bubble allows the status and URL strings to be set
229   // independently.  Whichever was set non-empty most recently will be the
230   // value displayed.  When both are empty, the status bubble hides.
231
232   NSString* text_ns = base::SysUTF16ToNSString(text);
233
234   NSString** main;
235   NSString** backup;
236
237   if (is_url) {
238     main = &url_text_;
239     backup = &status_text_;
240   } else {
241     main = &status_text_;
242     backup = &url_text_;
243   }
244
245   // Don't return from this function early.  It's important to make sure that
246   // all calls to StartShowing and StartHiding are made, so that all delays
247   // are observed properly.  Specifically, if the state is currently
248   // kBubbleShowingTimer, the timer will need to be restarted even if
249   // [text_ns isEqualToString:*main] is true.
250
251   [*main autorelease];
252   *main = [text_ns retain];
253
254   bool show = true;
255   if ([*main length] > 0)
256     [[window_ contentView] setContent:*main];
257   else if ([*backup length] > 0)
258     [[window_ contentView] setContent:*backup];
259   else
260     show = false;
261
262   if (show) {
263     UpdateSizeAndPosition();
264     StartShowing();
265   } else {
266     StartHiding();
267   }
268 }
269
270 void StatusBubbleMac::Hide() {
271   CancelTimer();
272   CancelExpandTimer();
273   is_expanded_ = false;
274
275   bool fade_out = false;
276   if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) {
277     SetState(kBubbleHidingFadeOut);
278
279     if (!immediate_) {
280       // An animation is in progress.  Cancel it by starting a new animation.
281       // Use kMinimumTimeInterval to set the opacity as rapidly as possible.
282       fade_out = true;
283       AnimateWindowAlpha(0.0, kMinimumTimeInterval);
284     }
285   }
286
287   if (!fade_out) {
288     // No animation is in progress, so the opacity can be set directly.
289     [window_ setAlphaValue:0.0];
290     SetState(kBubbleHidden);
291   }
292
293   // Stop any width animation and reset the bubble size.
294   if (!immediate_) {
295     [NSAnimationContext beginGrouping];
296     [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
297     [[window_ animator] setFrame:CalculateWindowFrame(/*expand=*/false)
298                          display:NO];
299     [NSAnimationContext endGrouping];
300   } else {
301     [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
302   }
303
304   [status_text_ release];
305   status_text_ = nil;
306   [url_text_ release];
307   url_text_ = nil;
308 }
309
310 void StatusBubbleMac::SetFrameAvoidingMouse(
311     NSRect window_frame, const gfx::Point& mouse_pos) {
312   if (!window_)
313     return;
314
315   // Bubble's base rect in |parent_| (window base) coordinates.
316   NSRect base_rect;
317   if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
318     base_rect = [delegate_ statusBubbleBaseFrame];
319   } else {
320     base_rect = [[parent_ contentView] bounds];
321     base_rect = [[parent_ contentView] convertRect:base_rect toView:nil];
322   }
323
324   // To start, assume default positioning in the lower left corner.
325   // The window_frame position is in global (screen) coordinates.
326   window_frame.origin = [parent_ convertBaseToScreen:base_rect.origin];
327
328   // Get the cursor position relative to the top right corner of the bubble.
329   gfx::Point relative_pos(mouse_pos.x() - NSMaxX(window_frame),
330                           mouse_pos.y() - NSMaxY(window_frame));
331
332   // If the mouse is in a position where we think it would move the
333   // status bubble, figure out where and how the bubble should be moved, and
334   // what sorts of corners it should have.
335   unsigned long corner_flags;
336   if (relative_pos.y() < kMousePadding &&
337       relative_pos.x() < kMousePadding) {
338     int offset = kMousePadding - relative_pos.y();
339
340     // Make the movement non-linear.
341     offset = offset * offset / kMousePadding;
342
343     // When the mouse is entering from the right, we want the offset to be
344     // scaled by how horizontally far away the cursor is from the bubble.
345     if (relative_pos.x() > 0) {
346       offset *= (kMousePadding - relative_pos.x()) / kMousePadding;
347     }
348
349     bool is_on_screen = true;
350     NSScreen* screen = [window_ screen];
351     if (screen &&
352         NSMinY([screen visibleFrame]) > NSMinY(window_frame) - offset) {
353       is_on_screen = false;
354     }
355
356     // If something is shown below tab contents (devtools, download shelf etc.),
357     // adjust the position to sit on top of it.
358     bool is_any_shelf_visible = NSMinY(base_rect) > 0;
359
360     if (is_on_screen && !is_any_shelf_visible) {
361       // Cap the offset and change the visual presentation of the bubble
362       // depending on where it ends up (so that rounded corners square off
363       // and mate to the edges of the tab content).
364       if (offset >= NSHeight(window_frame)) {
365         offset = NSHeight(window_frame);
366         corner_flags = kRoundedBottomLeftCorner | kRoundedBottomRightCorner;
367       } else if (offset > 0) {
368         corner_flags = kRoundedTopRightCorner |
369                        kRoundedBottomLeftCorner |
370                        kRoundedBottomRightCorner;
371       } else {
372         corner_flags = kRoundedTopRightCorner;
373       }
374
375       // Place the bubble on the left, but slightly lower.
376       window_frame.origin.y -= offset;
377     } else {
378       // Cannot move the bubble down without obscuring other content.
379       // Move it to the far right instead.
380       corner_flags = kRoundedTopLeftCorner;
381       window_frame.origin.x += NSWidth(base_rect) - NSWidth(window_frame);
382     }
383   } else {
384     // Use the default position in the lower left corner of the content area.
385     corner_flags = kRoundedTopRightCorner;
386   }
387
388   corner_flags |= OSDependentCornerFlags(window_frame);
389
390   [[window_ contentView] setCornerFlags:corner_flags];
391   [window_ setFrame:window_frame display:YES];
392 }
393
394 void StatusBubbleMac::MouseMoved(
395     const gfx::Point& location, bool left_content) {
396   if (!left_content)
397     SetFrameAvoidingMouse([window_ frame], location);
398 }
399
400 void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) {
401   UpdateSizeAndPosition();
402 }
403
404 void StatusBubbleMac::Create() {
405   DCHECK(!window_);
406
407   window_ = [[StatusBubbleWindow alloc]
408       initWithContentRect:ui::kWindowSizeDeterminedLater
409                 styleMask:NSBorderlessWindowMask
410                   backing:NSBackingStoreBuffered
411                     defer:YES];
412   [window_ setMovableByWindowBackground:NO];
413   [window_ setBackgroundColor:[NSColor clearColor]];
414   [window_ setLevel:NSNormalWindowLevel];
415   [window_ setOpaque:NO];
416   [window_ setHasShadow:NO];
417
418   // We do not need to worry about the bubble outliving |parent_| because our
419   // teardown sequence in BWC guarantees that |parent_| outlives the status
420   // bubble and that the StatusBubble is torn down completely prior to the
421   // window going away.
422   base::scoped_nsobject<BubbleView> view(
423       [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]);
424   [window_ setContentView:view];
425
426   [window_ setAlphaValue:0.0];
427
428   // TODO(dtseng): Ignore until we provide NSAccessibility support.
429   [window_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
430                             forAttribute:NSAccessibilityRoleAttribute];
431
432   [view setCornerFlags:kRoundedTopRightCorner];
433   MouseMoved(gfx::Point(), false);
434 }
435
436 void StatusBubbleMac::Attach() {
437   DCHECK(!is_attached());
438
439   [window_ orderFront:nil];
440   [parent_ addChildWindow:window_ ordered:NSWindowAbove];
441
442   [[window_ contentView] setThemeProvider:parent_];
443 }
444
445 void StatusBubbleMac::Detach() {
446   DCHECK(is_attached());
447
448   // Magic setFrame: See http://crbug.com/58506 and http://crrev.com/3564021 .
449   [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
450   [parent_ removeChildWindow:window_];  // See crbug.com/28107 ...
451   [window_ orderOut:nil];               // ... and crbug.com/29054.
452
453   [[window_ contentView] setThemeProvider:nil];
454 }
455
456 void StatusBubbleMac::AnimationDidStop() {
457   DCHECK([NSThread isMainThread]);
458   DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut);
459   DCHECK(is_attached());
460
461   if (state_ == kBubbleShowingFadeIn) {
462     DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
463     SetState(kBubbleShown);
464   } else {
465     DCHECK_EQ([[window_ animator] alphaValue], 0.0);
466     SetState(kBubbleHidden);
467   }
468 }
469
470 void StatusBubbleMac::SetState(StatusBubbleState state) {
471   if (state == state_)
472     return;
473
474   if (state == kBubbleHidden) {
475     // When hidden (with alpha of 0), make the window have the minimum size,
476     // while still keeping the same origin. It's important to not set the
477     // origin to 0,0 as that will cause the window to use more space in
478     // Expose/Mission Control. See http://crbug.com/81969.
479     //
480     // Also, doing it this way instead of detaching the window avoids bugs with
481     // Spaces and Cmd-`. See http://crbug.com/31821 and http://crbug.com/61629.
482     NSRect frame = [window_ frame];
483     frame.size = ui::kWindowSizeDeterminedLater.size;
484     [window_ setFrame:frame display:YES];
485   }
486
487   if ([delegate_ respondsToSelector:@selector(statusBubbleWillEnterState:)])
488     [delegate_ statusBubbleWillEnterState:state];
489
490   state_ = state;
491 }
492
493 void StatusBubbleMac::Fade(bool show) {
494   DCHECK([NSThread isMainThread]);
495
496   StatusBubbleState fade_state = kBubbleShowingFadeIn;
497   StatusBubbleState target_state = kBubbleShown;
498   NSTimeInterval full_duration = kShowFadeInDurationSeconds;
499   CGFloat opacity = kBubbleOpacity;
500
501   if (!show) {
502     fade_state = kBubbleHidingFadeOut;
503     target_state = kBubbleHidden;
504     full_duration = kHideFadeOutDurationSeconds;
505     opacity = 0.0;
506   }
507
508   DCHECK(state_ == fade_state || state_ == target_state);
509
510   if (state_ == target_state)
511     return;
512
513   if (immediate_) {
514     [window_ setAlphaValue:opacity];
515     SetState(target_state);
516     return;
517   }
518
519   // If an incomplete transition has left the opacity somewhere between 0 and
520   // kBubbleOpacity, the fade rate is kept constant by shortening the duration.
521   NSTimeInterval duration =
522       full_duration *
523       fabs(opacity - [[window_ animator] alphaValue]) / kBubbleOpacity;
524
525   // 0.0 will not cancel an in-progress animation.
526   if (duration == 0.0)
527     duration = kMinimumTimeInterval;
528
529   // Cancel an in-progress transition and replace it with this fade.
530   AnimateWindowAlpha(opacity, duration);
531 }
532
533 void StatusBubbleMac::AnimateWindowAlpha(CGFloat alpha,
534                                          NSTimeInterval duration) {
535   completion_handler_factory_.InvalidateWeakPtrs();
536   base::WeakPtr<StatusBubbleMac> weak_ptr(
537       completion_handler_factory_.GetWeakPtr());
538   [window_
539       runAnimationGroup:^(NSAnimationContext* context) {
540           [context setDuration:duration];
541           [[window_ animator] setAlphaValue:alpha];
542       }
543       completionHandler:^{
544           if (weak_ptr)
545             weak_ptr->AnimationDidStop();
546       }];
547 }
548
549 void StatusBubbleMac::StartTimer(int64 delay_ms) {
550   DCHECK([NSThread isMainThread]);
551   DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
552
553   if (immediate_) {
554     TimerFired();
555     return;
556   }
557
558   // There can only be one running timer.
559   CancelTimer();
560
561   base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
562       base::Bind(&StatusBubbleMac::TimerFired, timer_factory_.GetWeakPtr()),
563       base::TimeDelta::FromMilliseconds(delay_ms));
564 }
565
566 void StatusBubbleMac::CancelTimer() {
567   DCHECK([NSThread isMainThread]);
568
569   if (timer_factory_.HasWeakPtrs())
570     timer_factory_.InvalidateWeakPtrs();
571 }
572
573 void StatusBubbleMac::TimerFired() {
574   DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
575   DCHECK([NSThread isMainThread]);
576
577   if (state_ == kBubbleShowingTimer) {
578     SetState(kBubbleShowingFadeIn);
579     Fade(true);
580   } else {
581     SetState(kBubbleHidingFadeOut);
582     Fade(false);
583   }
584 }
585
586 void StatusBubbleMac::StartShowing() {
587   if (state_ == kBubbleHidden) {
588     // Arrange to begin fading in after a delay.
589     SetState(kBubbleShowingTimer);
590     StartTimer(kShowDelayMS);
591   } else if (state_ == kBubbleHidingFadeOut) {
592     // Cancel the fade-out in progress and replace it with a fade in.
593     SetState(kBubbleShowingFadeIn);
594     Fade(true);
595   } else if (state_ == kBubbleHidingTimer) {
596     // The bubble was already shown but was waiting to begin fading out.  It's
597     // given a stay of execution.
598     SetState(kBubbleShown);
599     CancelTimer();
600   } else if (state_ == kBubbleShowingTimer) {
601     // The timer was already running but nothing was showing yet.  Reaching
602     // this point means that there is a new request to show something.  Start
603     // over again by resetting the timer, effectively invalidating the earlier
604     // request.
605     StartTimer(kShowDelayMS);
606   }
607
608   // If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything
609   // alone.
610 }
611
612 void StatusBubbleMac::StartHiding() {
613   if (state_ == kBubbleShown) {
614     // Arrange to begin fading out after a delay.
615     SetState(kBubbleHidingTimer);
616     StartTimer(kHideDelayMS);
617   } else if (state_ == kBubbleShowingFadeIn) {
618     // Cancel the fade-in in progress and replace it with a fade out.
619     SetState(kBubbleHidingFadeOut);
620     Fade(false);
621   } else if (state_ == kBubbleShowingTimer) {
622     // The bubble was already hidden but was waiting to begin fading in.  Too
623     // bad, it won't get the opportunity now.
624     SetState(kBubbleHidden);
625     CancelTimer();
626   }
627
628   // If the state is kBubbleHidden, kBubbleHidingFadeOut, or
629   // kBubbleHidingTimer, leave everything alone.  The timer is not reset as
630   // with kBubbleShowingTimer in StartShowing() because a subsequent request
631   // to hide something while one is already in flight does not invalidate the
632   // earlier request.
633 }
634
635 void StatusBubbleMac::CancelExpandTimer() {
636   DCHECK([NSThread isMainThread]);
637   expand_timer_factory_.InvalidateWeakPtrs();
638 }
639
640 // Get the current location of the mouse in screen coordinates. To make this
641 // class testable, all code should use this method rather than using
642 // NSEvent mouseLocation directly.
643 gfx::Point StatusBubbleMac::GetMouseLocation() {
644   NSPoint p = [NSEvent mouseLocation];
645   --p.y;  // The docs say the y coord starts at 1 not 0; don't ask why.
646   return gfx::Point(p.x, p.y);
647 }
648
649 void StatusBubbleMac::ExpandBubble() {
650   // Calculate the width available for expanded and standard bubbles.
651   NSRect window_frame = CalculateWindowFrame(/*expand=*/true);
652   CGFloat max_bubble_width = NSWidth(window_frame);
653   CGFloat standard_bubble_width =
654       NSWidth(CalculateWindowFrame(/*expand=*/false));
655
656   // Generate the URL string that fits in the expanded bubble.
657   NSFont* font = [[window_ contentView] font];
658   gfx::FontList font_list_chr(
659       gfx::Font(base::SysNSStringToUTF8([font fontName]), [font pointSize]));
660   base::string16 expanded_url = ElideUrl(
661       url_, font_list_chr, max_bubble_width, languages_);
662
663   // Scale width from gfx::Font in view coordinates to window coordinates.
664   int required_width_for_string =
665       gfx::GetStringWidth(expanded_url, font_list_chr) +
666           kTextPadding * 2 + kBubbleViewTextPositionX;
667   NSSize scaled_width = NSMakeSize(required_width_for_string, 0);
668   scaled_width = [[parent_ contentView] convertSize:scaled_width toView:nil];
669   required_width_for_string = scaled_width.width;
670
671   // The expanded width must be at least as wide as the standard width, but no
672   // wider than the maximum width for its parent frame.
673   int expanded_bubble_width =
674       std::max(standard_bubble_width,
675                std::min(max_bubble_width,
676                         static_cast<CGFloat>(required_width_for_string)));
677
678   SetText(expanded_url, true);
679   is_expanded_ = true;
680   window_frame.size.width = expanded_bubble_width;
681
682   // In testing, don't do any animation.
683   if (immediate_) {
684     [window_ setFrame:window_frame display:YES];
685     return;
686   }
687
688   NSRect actual_window_frame = [window_ frame];
689   // Adjust status bubble origin if bubble was moved to the right.
690   // TODO(alekseys): fix for RTL.
691   if (NSMinX(actual_window_frame) > NSMinX(window_frame)) {
692     actual_window_frame.origin.x =
693         NSMaxX(actual_window_frame) - NSWidth(window_frame);
694   }
695   actual_window_frame.size.width = NSWidth(window_frame);
696
697   // Do not expand if it's going to cover mouse location.
698   gfx::Point p = GetMouseLocation();
699   if (NSPointInRect(NSMakePoint(p.x(), p.y()), actual_window_frame))
700     return;
701
702   // Get the current corner flags and see what needs to change based on the
703   // expansion. This is only needed on Lion, which has rounded window bottoms.
704   if (base::mac::IsOSLionOrLater()) {
705     unsigned long corner_flags = [[window_ contentView] cornerFlags];
706     corner_flags |= OSDependentCornerFlags(actual_window_frame);
707     [[window_ contentView] setCornerFlags:corner_flags];
708   }
709
710   [NSAnimationContext beginGrouping];
711   [[NSAnimationContext currentContext] setDuration:kExpansionDurationSeconds];
712   [[window_ animator] setFrame:actual_window_frame display:YES];
713   [NSAnimationContext endGrouping];
714 }
715
716 void StatusBubbleMac::UpdateSizeAndPosition() {
717   if (!window_)
718     return;
719
720   SetFrameAvoidingMouse(CalculateWindowFrame(/*expand=*/false),
721                         GetMouseLocation());
722 }
723
724 void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) {
725   DCHECK(parent);
726   DCHECK(is_attached());
727
728   Detach();
729   parent_ = parent;
730   Attach();
731   UpdateSizeAndPosition();
732 }
733
734 NSRect StatusBubbleMac::CalculateWindowFrame(bool expanded_width) {
735   DCHECK(parent_);
736
737   NSRect screenRect;
738   if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
739     screenRect = [delegate_ statusBubbleBaseFrame];
740     screenRect.origin = [parent_ convertBaseToScreen:screenRect.origin];
741   } else {
742     screenRect = [parent_ frame];
743   }
744
745   NSSize size = NSMakeSize(0, kWindowHeight);
746   size = [[parent_ contentView] convertSize:size toView:nil];
747
748   if (expanded_width) {
749     size.width = screenRect.size.width;
750   } else {
751     size.width = kWindowWidthPercent * screenRect.size.width;
752   }
753
754   screenRect.size = size;
755   return screenRect;
756 }
757
758 unsigned long StatusBubbleMac::OSDependentCornerFlags(NSRect window_frame) {
759   unsigned long corner_flags = 0;
760
761   if (base::mac::IsOSLionOrLater()) {
762     NSRect parent_frame = [parent_ frame];
763
764     // Round the bottom corners when they're right up against the
765     // corresponding edge of the parent window, or when below the parent
766     // window.
767     if (NSMinY(window_frame) <= NSMinY(parent_frame)) {
768       if (NSMinX(window_frame) == NSMinX(parent_frame)) {
769         corner_flags |= kRoundedBottomLeftCorner;
770       }
771
772       if (NSMaxX(window_frame) == NSMaxX(parent_frame)) {
773         corner_flags |= kRoundedBottomRightCorner;
774       }
775     }
776
777     // Round the top corners when the bubble is below the parent window.
778     if (NSMinY(window_frame) < NSMinY(parent_frame)) {
779       corner_flags |= kRoundedTopLeftCorner | kRoundedTopRightCorner;
780     }
781   }
782
783   return corner_flags;
784 }