Don't show supervised user as "already on this device" while they're being imported.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / status_bubble_mac.mm
blob7380c06454e64c4eb3c4887a4bb1e558f776d954
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 "chrome/browser/ui/elide_url.h"
21 #include "net/base/net_util.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/text_elider.h"
29 #include "ui/gfx/text_utils.h"
31 namespace {
33 const int kWindowHeight = 18;
35 // The width of the bubble in relation to the width of the parent window.
36 const CGFloat kWindowWidthPercent = 1.0 / 3.0;
38 // How close the mouse can get to the infobubble before it starts sliding
39 // off-screen.
40 const int kMousePadding = 20;
42 const int kTextPadding = 3;
44 // The status bubble's maximum opacity, when fully faded in.
45 const CGFloat kBubbleOpacity = 1.0;
47 // Delay before showing or hiding the bubble after a SetStatus or SetURL call.
48 const int64 kShowDelayMS = 80;
49 const int64 kHideDelayMS = 250;
51 // How long each fade should last.
52 const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
53 const NSTimeInterval kHideFadeOutDurationSeconds = 0.200;
55 // The minimum representable time interval.  This can be used as the value
56 // passed to +[NSAnimationContext setDuration:] to stop an in-progress
57 // animation as quickly as possible.
58 const NSTimeInterval kMinimumTimeInterval =
59     std::numeric_limits<NSTimeInterval>::min();
61 // How quickly the status bubble should expand.
62 const CGFloat kExpansionDurationSeconds = 0.125;
64 }  // namespace
66 @interface StatusBubbleAnimationDelegate : NSObject {
67  @private
68   base::mac::ScopedBlock<void (^)(void)> completionHandler_;
71 - (id)initWithCompletionHandler:(void (^)(void))completionHandler;
73 // CAAnimation delegate method
74 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
75 @end
77 @implementation StatusBubbleAnimationDelegate
79 - (id)initWithCompletionHandler:(void (^)(void))completionHandler {
80   if ((self = [super init])) {
81     completionHandler_.reset(completionHandler, base::scoped_policy::RETAIN);
82   }
84   return self;
87 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
88   completionHandler_.get()();
91 @end
93 @interface StatusBubbleWindow : NSWindow {
94  @private
95   void (^completionHandler_)(void);
98 - (id)animationForKey:(NSString *)key;
99 - (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
100         completionHandler:(void (^)(void))completionHandler;
101 @end
103 @implementation StatusBubbleWindow
105 - (id)animationForKey:(NSString *)key {
106   CAAnimation* animation = [super animationForKey:key];
107   // If completionHandler_ isn't nil, then this is the first of (potentially)
108   // multiple animations in a grouping; give it the completion handler. If
109   // completionHandler_ is nil, then some other animation was tagged with the
110   // completion handler.
111   if (completionHandler_) {
112     DCHECK(![NSAnimationContext respondsToSelector:
113                @selector(runAnimationGroup:completionHandler:)]);
114     StatusBubbleAnimationDelegate* animation_delegate =
115         [[StatusBubbleAnimationDelegate alloc]
116              initWithCompletionHandler:completionHandler_];
117     [animation setDelegate:animation_delegate];
118     completionHandler_ = nil;
119   }
120   return animation;
123 - (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
124         completionHandler:(void (^)(void))completionHandler {
125   if ([NSAnimationContext respondsToSelector:
126           @selector(runAnimationGroup:completionHandler:)]) {
127     [NSAnimationContext runAnimationGroup:changes
128                         completionHandler:completionHandler];
129   } else {
130     // Mac OS 10.6 does not have completion handler callbacks at the Cocoa
131     // level, only at the CoreAnimation level. So intercept calls made to
132     // -animationForKey: and tag one of the animations with a delegate that will
133     // execute the completion handler.
134     completionHandler_ = completionHandler;
135     [NSAnimationContext beginGrouping];
136     changes([NSAnimationContext currentContext]);
137     // At this point, -animationForKey should have been called by CoreAnimation
138     // to set up the animation to run. Verify this.
139     DCHECK(completionHandler_ == nil);
140     [NSAnimationContext endGrouping];
141   }
144 @end
146 // Mac implementation of the status bubble.
148 // Child windows interact with Spaces in interesting ways, so this code has to
149 // follow these rules:
151 // 1) NSWindows cannot have zero size.  At times when the status bubble window
152 //    has no specific size (for example, when hidden), its size is set to
153 //    ui::kWindowSizeDeterminedLater.
155 // 2) Child window frames are in the coordinate space of the screen, not of the
156 //    parent window.  If a child window has its origin at (0, 0), Spaces will
157 //    position it in the corner of the screen but group it with the parent
158 //    window in Spaces.  This causes Chrome windows to have a large (mostly
159 //    blank) area in Spaces.  To avoid this, child windows always have their
160 //    origin set to the lower-left corner of the window.
162 // 3) Detached child windows may show up as top-level windows in Spaces.  To
163 //    avoid this, once the status bubble is Attach()ed to the parent, it is
164 //    never detached (except in rare cases when reparenting to a fullscreen
165 //    window).
167 // 4) To avoid unnecessary redraws, if a bubble is in the kBubbleHidden state,
168 //    its size is always set to ui::kWindowSizeDeterminedLater.  The proper
169 //    width for the current URL or status text is not calculated until the
170 //    bubble leaves the kBubbleHidden state.
172 StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
173     : parent_(parent),
174       delegate_(delegate),
175       window_(nil),
176       status_text_(nil),
177       url_text_(nil),
178       state_(kBubbleHidden),
179       immediate_(false),
180       is_expanded_(false),
181       timer_factory_(this),
182       expand_timer_factory_(this),
183       completion_handler_factory_(this) {
184   Create();
185   Attach();
188 StatusBubbleMac::~StatusBubbleMac() {
189   DCHECK(window_);
191   Hide();
193   completion_handler_factory_.InvalidateWeakPtrs();
194   Detach();
195   [window_ release];
196   window_ = nil;
199 void StatusBubbleMac::SetStatus(const base::string16& status) {
200   SetText(status, false);
203 void StatusBubbleMac::SetURL(const GURL& url, const std::string& languages) {
204   url_ = url;
205   languages_ = languages;
207   CGFloat bubble_width = NSWidth([window_ frame]);
208   if (state_ == kBubbleHidden) {
209     // TODO(rohitrao): The window size is expected to be (1,1) whenever the
210     // window is hidden, but the GPU bots are hitting cases where this is not
211     // true.  Instead of enforcing this invariant with a DCHECK, add temporary
212     // logging to try and debug it and fix up the window size if needed.
213     // This logging is temporary and should be removed: crbug.com/467998
214     NSRect frame = [window_ frame];
215     if (!CGSizeEqualToSize(frame.size, ui::kWindowSizeDeterminedLater.size)) {
216       LOG(ERROR) << "Window size should be (1,1), but is instead ("
217                  << frame.size.width << "," << frame.size.height << ")";
218       LOG(ERROR) << base::debug::StackTrace().ToString();
219       frame.size = ui::kWindowSizeDeterminedLater.size;
220       [window_ setFrame:frame display:NO];
221     }
222     bubble_width = NSWidth(CalculateWindowFrame(/*expand=*/false));
223   }
225   int text_width = static_cast<int>(bubble_width -
226                                     kBubbleViewTextPositionX -
227                                     kTextPadding);
229   // Scale from view to window coordinates before eliding URL string.
230   NSSize scaled_width = NSMakeSize(text_width, 0);
231   scaled_width = [[parent_ contentView] convertSize:scaled_width fromView:nil];
232   text_width = static_cast<int>(scaled_width.width);
233   NSFont* font = [[window_ contentView] font];
234   gfx::FontList font_list_chr(
235       gfx::Font(base::SysNSStringToUTF8([font fontName]), [font pointSize]));
237   base::string16 original_url_text = net::FormatUrl(url, languages);
238   base::string16 status =
239       ElideUrl(url, font_list_chr, text_width, languages);
241   SetText(status, true);
243   // In testing, don't use animation. When ExpandBubble is tested, it is
244   // called explicitly.
245   if (immediate_)
246     return;
247   else
248     CancelExpandTimer();
250   // If the bubble has been expanded, the user has already hovered over a link
251   // to trigger the expanded state.  Don't wait to change the bubble in this
252   // case -- immediately expand or contract to fit the URL.
253   if (is_expanded_ && !url.is_empty()) {
254     ExpandBubble();
255   } else if (original_url_text.length() > status.length()) {
256     base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
257         base::Bind(&StatusBubbleMac::ExpandBubble,
258                    expand_timer_factory_.GetWeakPtr()),
259         base::TimeDelta::FromMilliseconds(kExpandHoverDelayMS));
260   }
263 void StatusBubbleMac::SetText(const base::string16& text, bool is_url) {
264   // The status bubble allows the status and URL strings to be set
265   // independently.  Whichever was set non-empty most recently will be the
266   // value displayed.  When both are empty, the status bubble hides.
268   NSString* text_ns = base::SysUTF16ToNSString(text);
270   NSString** main;
271   NSString** backup;
273   if (is_url) {
274     main = &url_text_;
275     backup = &status_text_;
276   } else {
277     main = &status_text_;
278     backup = &url_text_;
279   }
281   // Don't return from this function early.  It's important to make sure that
282   // all calls to StartShowing and StartHiding are made, so that all delays
283   // are observed properly.  Specifically, if the state is currently
284   // kBubbleShowingTimer, the timer will need to be restarted even if
285   // [text_ns isEqualToString:*main] is true.
287   [*main autorelease];
288   *main = [text_ns retain];
290   bool show = true;
291   if ([*main length] > 0)
292     [[window_ contentView] setContent:*main];
293   else if ([*backup length] > 0)
294     [[window_ contentView] setContent:*backup];
295   else
296     show = false;
298   if (show) {
299     // Call StartShowing() first to update the current bubble state before
300     // calculating a new size.
301     StartShowing();
302     UpdateSizeAndPosition();
303   } else {
304     StartHiding();
305   }
308 void StatusBubbleMac::Hide() {
309   CancelTimer();
310   CancelExpandTimer();
311   is_expanded_ = false;
313   bool fade_out = false;
314   if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) {
315     SetState(kBubbleHidingFadeOut);
317     if (!immediate_) {
318       // An animation is in progress.  Cancel it by starting a new animation.
319       // Use kMinimumTimeInterval to set the opacity as rapidly as possible.
320       fade_out = true;
321       AnimateWindowAlpha(0.0, kMinimumTimeInterval);
322     }
323   }
325   NSRect frame = CalculateWindowFrame(/*expand=*/false);
326   if (!fade_out) {
327     // No animation is in progress, so the opacity can be set directly.
328     [window_ setAlphaValue:0.0];
329     SetState(kBubbleHidden);
330     frame.size = ui::kWindowSizeDeterminedLater.size;
331   }
333   // Stop any width animation and reset the bubble size.
334   if (!immediate_) {
335     [NSAnimationContext beginGrouping];
336     [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
337     [[window_ animator] setFrame:frame display:NO];
338     [NSAnimationContext endGrouping];
339   } else {
340     [window_ setFrame:frame display:NO];
341   }
343   [status_text_ release];
344   status_text_ = nil;
345   [url_text_ release];
346   url_text_ = nil;
349 void StatusBubbleMac::SetFrameAvoidingMouse(
350     NSRect window_frame, const gfx::Point& mouse_pos) {
351   if (!window_)
352     return;
354   // Bubble's base rect in |parent_| (window base) coordinates.
355   NSRect base_rect;
356   if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
357     base_rect = [delegate_ statusBubbleBaseFrame];
358   } else {
359     base_rect = [[parent_ contentView] bounds];
360     base_rect = [[parent_ contentView] convertRect:base_rect toView:nil];
361   }
363   // To start, assume default positioning in the lower left corner.
364   // The window_frame position is in global (screen) coordinates.
365   window_frame.origin = [parent_ convertBaseToScreen:base_rect.origin];
367   // Get the cursor position relative to the top right corner of the bubble.
368   gfx::Point relative_pos(mouse_pos.x() - NSMaxX(window_frame),
369                           mouse_pos.y() - NSMaxY(window_frame));
371   // If the mouse is in a position where we think it would move the
372   // status bubble, figure out where and how the bubble should be moved, and
373   // what sorts of corners it should have.
374   unsigned long corner_flags;
375   if (relative_pos.y() < kMousePadding &&
376       relative_pos.x() < kMousePadding) {
377     int offset = kMousePadding - relative_pos.y();
379     // Make the movement non-linear.
380     offset = offset * offset / kMousePadding;
382     // When the mouse is entering from the right, we want the offset to be
383     // scaled by how horizontally far away the cursor is from the bubble.
384     if (relative_pos.x() > 0) {
385       offset *= (kMousePadding - relative_pos.x()) / kMousePadding;
386     }
388     bool is_on_screen = true;
389     NSScreen* screen = [window_ screen];
390     if (screen &&
391         NSMinY([screen visibleFrame]) > NSMinY(window_frame) - offset) {
392       is_on_screen = false;
393     }
395     // If something is shown below tab contents (devtools, download shelf etc.),
396     // adjust the position to sit on top of it.
397     bool is_any_shelf_visible = NSMinY(base_rect) > 0;
399     if (is_on_screen && !is_any_shelf_visible) {
400       // Cap the offset and change the visual presentation of the bubble
401       // depending on where it ends up (so that rounded corners square off
402       // and mate to the edges of the tab content).
403       if (offset >= NSHeight(window_frame)) {
404         offset = NSHeight(window_frame);
405         corner_flags = kRoundedBottomLeftCorner | kRoundedBottomRightCorner;
406       } else if (offset > 0) {
407         corner_flags = kRoundedTopRightCorner |
408                        kRoundedBottomLeftCorner |
409                        kRoundedBottomRightCorner;
410       } else {
411         corner_flags = kRoundedTopRightCorner;
412       }
414       // Place the bubble on the left, but slightly lower.
415       window_frame.origin.y -= offset;
416     } else {
417       // Cannot move the bubble down without obscuring other content.
418       // Move it to the far right instead.
419       corner_flags = kRoundedTopLeftCorner;
420       window_frame.origin.x += NSWidth(base_rect) - NSWidth(window_frame);
421     }
422   } else {
423     // Use the default position in the lower left corner of the content area.
424     corner_flags = kRoundedTopRightCorner;
425   }
427   corner_flags |= OSDependentCornerFlags(window_frame);
429   [[window_ contentView] setCornerFlags:corner_flags];
430   [window_ setFrame:window_frame display:YES];
433 void StatusBubbleMac::MouseMoved(
434     const gfx::Point& location, bool left_content) {
435   if (!left_content)
436     SetFrameAvoidingMouse([window_ frame], location);
439 void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) {
440   UpdateSizeAndPosition();
443 void StatusBubbleMac::Create() {
444   DCHECK(!window_);
446   window_ = [[StatusBubbleWindow alloc]
447       initWithContentRect:ui::kWindowSizeDeterminedLater
448                 styleMask:NSBorderlessWindowMask
449                   backing:NSBackingStoreBuffered
450                     defer:YES];
451   [window_ setMovableByWindowBackground:NO];
452   [window_ setBackgroundColor:[NSColor clearColor]];
453   [window_ setLevel:NSNormalWindowLevel];
454   [window_ setOpaque:NO];
455   [window_ setHasShadow:NO];
457   // We do not need to worry about the bubble outliving |parent_| because our
458   // teardown sequence in BWC guarantees that |parent_| outlives the status
459   // bubble and that the StatusBubble is torn down completely prior to the
460   // window going away.
461   base::scoped_nsobject<BubbleView> view(
462       [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]);
463   [window_ setContentView:view];
465   [window_ setAlphaValue:0.0];
467   // TODO(dtseng): Ignore until we provide NSAccessibility support.
468   [window_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
469                             forAttribute:NSAccessibilityRoleAttribute];
471   [view setCornerFlags:kRoundedTopRightCorner];
472   MouseMoved(gfx::Point(), false);
475 void StatusBubbleMac::Attach() {
476   DCHECK(!is_attached());
478   [window_ orderFront:nil];
479   [parent_ addChildWindow:window_ ordered:NSWindowAbove];
481   [[window_ contentView] setThemeProvider:parent_];
484 void StatusBubbleMac::Detach() {
485   DCHECK(is_attached());
487   // Magic setFrame: See http://crbug.com/58506 and http://crrev.com/3564021 .
488   // TODO(rohitrao): Does the frame size actually matter here?  Can we always
489   // set it to kWindowSizeDeterminedLater?
490   NSRect frame = [window_ frame];
491   frame.size = ui::kWindowSizeDeterminedLater.size;
492   if (state_ != kBubbleHidden) {
493     frame = CalculateWindowFrame(/*expand=*/false);
494   }
495   [window_ setFrame:frame display:NO];
496   [parent_ removeChildWindow:window_];  // See crbug.com/28107 ...
497   [window_ orderOut:nil];               // ... and crbug.com/29054.
499   [[window_ contentView] setThemeProvider:nil];
502 void StatusBubbleMac::AnimationDidStop() {
503   DCHECK([NSThread isMainThread]);
504   DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut);
505   DCHECK(is_attached());
507   if (state_ == kBubbleShowingFadeIn) {
508     DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
509     SetState(kBubbleShown);
510   } else {
511     DCHECK_EQ([[window_ animator] alphaValue], 0.0);
512     SetState(kBubbleHidden);
513   }
516 void StatusBubbleMac::SetState(StatusBubbleState state) {
517   if (state == state_)
518     return;
520   if (state == kBubbleHidden) {
521     is_expanded_ = false;
523     // When hidden (with alpha of 0), make the window have the minimum size,
524     // while still keeping the same origin. It's important to not set the
525     // origin to 0,0 as that will cause the window to use more space in
526     // Expose/Mission Control. See http://crbug.com/81969.
527     //
528     // Also, doing it this way instead of detaching the window avoids bugs with
529     // Spaces and Cmd-`. See http://crbug.com/31821 and http://crbug.com/61629.
530     NSRect frame = [window_ frame];
531     frame.size = ui::kWindowSizeDeterminedLater.size;
532     [window_ setFrame:frame display:YES];
533   }
535   if ([delegate_ respondsToSelector:@selector(statusBubbleWillEnterState:)])
536     [delegate_ statusBubbleWillEnterState:state];
538   state_ = state;
541 void StatusBubbleMac::Fade(bool show) {
542   DCHECK([NSThread isMainThread]);
544   StatusBubbleState fade_state = kBubbleShowingFadeIn;
545   StatusBubbleState target_state = kBubbleShown;
546   NSTimeInterval full_duration = kShowFadeInDurationSeconds;
547   CGFloat opacity = kBubbleOpacity;
549   if (!show) {
550     fade_state = kBubbleHidingFadeOut;
551     target_state = kBubbleHidden;
552     full_duration = kHideFadeOutDurationSeconds;
553     opacity = 0.0;
554   }
556   DCHECK(state_ == fade_state || state_ == target_state);
558   if (state_ == target_state)
559     return;
561   if (immediate_) {
562     [window_ setAlphaValue:opacity];
563     SetState(target_state);
564     return;
565   }
567   // If an incomplete transition has left the opacity somewhere between 0 and
568   // kBubbleOpacity, the fade rate is kept constant by shortening the duration.
569   NSTimeInterval duration =
570       full_duration *
571       fabs(opacity - [[window_ animator] alphaValue]) / kBubbleOpacity;
573   // 0.0 will not cancel an in-progress animation.
574   if (duration == 0.0)
575     duration = kMinimumTimeInterval;
577   // Cancel an in-progress transition and replace it with this fade.
578   AnimateWindowAlpha(opacity, duration);
581 void StatusBubbleMac::AnimateWindowAlpha(CGFloat alpha,
582                                          NSTimeInterval duration) {
583   completion_handler_factory_.InvalidateWeakPtrs();
584   base::WeakPtr<StatusBubbleMac> weak_ptr(
585       completion_handler_factory_.GetWeakPtr());
586   [window_
587       runAnimationGroup:^(NSAnimationContext* context) {
588           [context setDuration:duration];
589           [[window_ animator] setAlphaValue:alpha];
590       }
591       completionHandler:^{
592           if (weak_ptr)
593             weak_ptr->AnimationDidStop();
594       }];
597 void StatusBubbleMac::StartTimer(int64 delay_ms) {
598   DCHECK([NSThread isMainThread]);
599   DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
601   if (immediate_) {
602     TimerFired();
603     return;
604   }
606   // There can only be one running timer.
607   CancelTimer();
609   base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
610       base::Bind(&StatusBubbleMac::TimerFired, timer_factory_.GetWeakPtr()),
611       base::TimeDelta::FromMilliseconds(delay_ms));
614 void StatusBubbleMac::CancelTimer() {
615   DCHECK([NSThread isMainThread]);
617   if (timer_factory_.HasWeakPtrs())
618     timer_factory_.InvalidateWeakPtrs();
621 void StatusBubbleMac::TimerFired() {
622   DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
623   DCHECK([NSThread isMainThread]);
625   if (state_ == kBubbleShowingTimer) {
626     SetState(kBubbleShowingFadeIn);
627     Fade(true);
628   } else {
629     SetState(kBubbleHidingFadeOut);
630     Fade(false);
631   }
634 void StatusBubbleMac::StartShowing() {
635   if (state_ == kBubbleHidden) {
636     // Arrange to begin fading in after a delay.
637     SetState(kBubbleShowingTimer);
638     StartTimer(kShowDelayMS);
639   } else if (state_ == kBubbleHidingFadeOut) {
640     // Cancel the fade-out in progress and replace it with a fade in.
641     SetState(kBubbleShowingFadeIn);
642     Fade(true);
643   } else if (state_ == kBubbleHidingTimer) {
644     // The bubble was already shown but was waiting to begin fading out.  It's
645     // given a stay of execution.
646     SetState(kBubbleShown);
647     CancelTimer();
648   } else if (state_ == kBubbleShowingTimer) {
649     // The timer was already running but nothing was showing yet.  Reaching
650     // this point means that there is a new request to show something.  Start
651     // over again by resetting the timer, effectively invalidating the earlier
652     // request.
653     StartTimer(kShowDelayMS);
654   }
656   // If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything
657   // alone.
660 void StatusBubbleMac::StartHiding() {
661   if (state_ == kBubbleShown) {
662     // Arrange to begin fading out after a delay.
663     SetState(kBubbleHidingTimer);
664     StartTimer(kHideDelayMS);
665   } else if (state_ == kBubbleShowingFadeIn) {
666     // Cancel the fade-in in progress and replace it with a fade out.
667     SetState(kBubbleHidingFadeOut);
668     Fade(false);
669   } else if (state_ == kBubbleShowingTimer) {
670     // The bubble was already hidden but was waiting to begin fading in.  Too
671     // bad, it won't get the opportunity now.
672     SetState(kBubbleHidden);
673     CancelTimer();
674   }
676   // If the state is kBubbleHidden, kBubbleHidingFadeOut, or
677   // kBubbleHidingTimer, leave everything alone.  The timer is not reset as
678   // with kBubbleShowingTimer in StartShowing() because a subsequent request
679   // to hide something while one is already in flight does not invalidate the
680   // earlier request.
683 void StatusBubbleMac::CancelExpandTimer() {
684   DCHECK([NSThread isMainThread]);
685   expand_timer_factory_.InvalidateWeakPtrs();
688 // Get the current location of the mouse in screen coordinates. To make this
689 // class testable, all code should use this method rather than using
690 // NSEvent mouseLocation directly.
691 gfx::Point StatusBubbleMac::GetMouseLocation() {
692   NSPoint p = [NSEvent mouseLocation];
693   --p.y;  // The docs say the y coord starts at 1 not 0; don't ask why.
694   return gfx::Point(p.x, p.y);
697 void StatusBubbleMac::ExpandBubble() {
698   // Calculate the width available for expanded and standard bubbles.
699   NSRect window_frame = CalculateWindowFrame(/*expand=*/true);
700   CGFloat max_bubble_width = NSWidth(window_frame);
701   CGFloat standard_bubble_width =
702       NSWidth(CalculateWindowFrame(/*expand=*/false));
704   // Generate the URL string that fits in the expanded bubble.
705   NSFont* font = [[window_ contentView] font];
706   gfx::FontList font_list_chr(
707       gfx::Font(base::SysNSStringToUTF8([font fontName]), [font pointSize]));
708   base::string16 expanded_url = ElideUrl(
709       url_, font_list_chr, max_bubble_width, languages_);
711   // Scale width from gfx::Font in view coordinates to window coordinates.
712   int required_width_for_string =
713       gfx::GetStringWidth(expanded_url, font_list_chr) +
714           kTextPadding * 2 + kBubbleViewTextPositionX;
715   NSSize scaled_width = NSMakeSize(required_width_for_string, 0);
716   scaled_width = [[parent_ contentView] convertSize:scaled_width toView:nil];
717   required_width_for_string = scaled_width.width;
719   // The expanded width must be at least as wide as the standard width, but no
720   // wider than the maximum width for its parent frame.
721   int expanded_bubble_width =
722       std::max(standard_bubble_width,
723                std::min(max_bubble_width,
724                         static_cast<CGFloat>(required_width_for_string)));
726   SetText(expanded_url, true);
727   is_expanded_ = true;
728   window_frame.size.width = expanded_bubble_width;
730   // In testing, don't do any animation.
731   if (immediate_) {
732     [window_ setFrame:window_frame display:YES];
733     return;
734   }
736   NSRect actual_window_frame = [window_ frame];
737   // Adjust status bubble origin if bubble was moved to the right.
738   // TODO(alekseys): fix for RTL.
739   if (NSMinX(actual_window_frame) > NSMinX(window_frame)) {
740     actual_window_frame.origin.x =
741         NSMaxX(actual_window_frame) - NSWidth(window_frame);
742   }
743   actual_window_frame.size.width = NSWidth(window_frame);
745   // Do not expand if it's going to cover mouse location.
746   gfx::Point p = GetMouseLocation();
747   if (NSPointInRect(NSMakePoint(p.x(), p.y()), actual_window_frame))
748     return;
750   // Get the current corner flags and see what needs to change based on the
751   // expansion. This is only needed on Lion, which has rounded window bottoms.
752   if (base::mac::IsOSLionOrLater()) {
753     unsigned long corner_flags = [[window_ contentView] cornerFlags];
754     corner_flags |= OSDependentCornerFlags(actual_window_frame);
755     [[window_ contentView] setCornerFlags:corner_flags];
756   }
758   [NSAnimationContext beginGrouping];
759   [[NSAnimationContext currentContext] setDuration:kExpansionDurationSeconds];
760   [[window_ animator] setFrame:actual_window_frame display:YES];
761   [NSAnimationContext endGrouping];
764 void StatusBubbleMac::UpdateSizeAndPosition() {
765   if (!window_)
766     return;
768   // There is no need to update the size if the bubble is hidden.
769   if (state_ == kBubbleHidden) {
770     // Verify that hidden bubbles always have size equal to
771     // ui::kWindowSizeDeterminedLater.
773     // TODO(rohitrao): The GPU bots are hitting cases where this is not true.
774     // Instead of enforcing this invariant with a DCHECK, add temporary logging
775     // to try and debug it and fix up the window size if needed.
776     // This logging is temporary and should be removed: crbug.com/467998
777     NSRect frame = [window_ frame];
778     if (!CGSizeEqualToSize(frame.size, ui::kWindowSizeDeterminedLater.size)) {
779       LOG(ERROR) << "Window size should be (1,1), but is instead ("
780                  << frame.size.width << "," << frame.size.height << ")";
781       LOG(ERROR) << base::debug::StackTrace().ToString();
782       frame.size = ui::kWindowSizeDeterminedLater.size;
783     }
785     // During the fullscreen animation, the parent window's origin may change
786     // without updating the status bubble.  To avoid animation glitches, always
787     // update the bubble's origin to match the parent's, even when hidden.
788     frame.origin = [parent_ frame].origin;
789     [window_ setFrame:frame display:NO];
790     return;
791   }
793   SetFrameAvoidingMouse(CalculateWindowFrame(/*expand=*/false),
794                         GetMouseLocation());
797 void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) {
798   DCHECK(parent);
799   DCHECK(is_attached());
801   Detach();
802   parent_ = parent;
803   Attach();
804   UpdateSizeAndPosition();
807 NSRect StatusBubbleMac::CalculateWindowFrame(bool expanded_width) {
808   DCHECK(parent_);
810   NSRect screenRect;
811   if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
812     screenRect = [delegate_ statusBubbleBaseFrame];
813     screenRect.origin = [parent_ convertBaseToScreen:screenRect.origin];
814   } else {
815     screenRect = [parent_ frame];
816   }
818   NSSize size = NSMakeSize(0, kWindowHeight);
819   size = [[parent_ contentView] convertSize:size toView:nil];
821   if (expanded_width) {
822     size.width = screenRect.size.width;
823   } else {
824     size.width = kWindowWidthPercent * screenRect.size.width;
825   }
827   screenRect.size = size;
828   return screenRect;
831 unsigned long StatusBubbleMac::OSDependentCornerFlags(NSRect window_frame) {
832   unsigned long corner_flags = 0;
834   if (base::mac::IsOSLionOrLater()) {
835     NSRect parent_frame = [parent_ frame];
837     // Round the bottom corners when they're right up against the
838     // corresponding edge of the parent window, or when below the parent
839     // window.
840     if (NSMinY(window_frame) <= NSMinY(parent_frame)) {
841       if (NSMinX(window_frame) == NSMinX(parent_frame)) {
842         corner_flags |= kRoundedBottomLeftCorner;
843       }
845       if (NSMaxX(window_frame) == NSMaxX(parent_frame)) {
846         corner_flags |= kRoundedBottomRightCorner;
847       }
848     }
850     // Round the top corners when the bubble is below the parent window.
851     if (NSMinY(window_frame) < NSMinY(parent_frame)) {
852       corner_flags |= kRoundedTopLeftCorner | kRoundedTopRightCorner;
853     }
854   }
856   return corner_flags;