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