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