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