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