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