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