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
30namespace {
31
32const int kWindowHeight = 18;
33
34// The width of the bubble in relation to the width of the parent window.
35const CGFloat kWindowWidthPercent = 1.0 / 3.0;
36
37// How close the mouse can get to the infobubble before it starts sliding
38// off-screen.
39const int kMousePadding = 20;
40
41const int kTextPadding = 3;
42
43// The status bubble's maximum opacity, when fully faded in.
44const CGFloat kBubbleOpacity = 1.0;
45
46// Delay before showing or hiding the bubble after a SetStatus or SetURL call.
47const int64 kShowDelayMS = 80;
48const int64 kHideDelayMS = 250;
49
50// How long each fade should last.
51const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
52const 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.
57const NSTimeInterval kMinimumTimeInterval =
58    std::numeric_limits<NSTimeInterval>::min();
59
60// How quickly the status bubble should expand.
61const 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
145StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
146    : parent_(parent),
147      delegate_(delegate),
148      window_(nil),
149      status_text_(nil),
150      url_text_(nil),
151      state_(kBubbleHidden),
152      immediate_(false),
153      is_expanded_(false),
154      timer_factory_(this),
155      expand_timer_factory_(this),
156      completion_handler_factory_(this) {
157  Create();
158  Attach();
159}
160
161StatusBubbleMac::~StatusBubbleMac() {
162  DCHECK(window_);
163
164  Hide();
165
166  completion_handler_factory_.InvalidateWeakPtrs();
167  Detach();
168  [window_ release];
169  window_ = nil;
170}
171
172void StatusBubbleMac::SetStatus(const base::string16& status) {
173  SetText(status, false);
174}
175
176void 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
227void 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
270void 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
310void 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
394void StatusBubbleMac::MouseMoved(
395    const gfx::Point& location, bool left_content) {
396  if (!left_content)
397    SetFrameAvoidingMouse([window_ frame], location);
398}
399
400void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) {
401  UpdateSizeAndPosition();
402}
403
404void 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
436void 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
445void 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
456void 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
470void 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
493void 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
533void 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
549void 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
566void StatusBubbleMac::CancelTimer() {
567  DCHECK([NSThread isMainThread]);
568
569  if (timer_factory_.HasWeakPtrs())
570    timer_factory_.InvalidateWeakPtrs();
571}
572
573void 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
586void 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
612void 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
635void 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.
643gfx::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
649void 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
716void StatusBubbleMac::UpdateSizeAndPosition() {
717  if (!window_)
718    return;
719
720  SetFrameAvoidingMouse(CalculateWindowFrame(/*expand=*/false),
721                        GetMouseLocation());
722}
723
724void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) {
725  DCHECK(parent);
726  DCHECK(is_attached());
727
728  Detach();
729  parent_ = parent;
730  Attach();
731  UpdateSizeAndPosition();
732}
733
734NSRect 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
758unsigned 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}
785