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