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