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