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.
5 #include "chrome/browser/ui/cocoa/status_bubble_mac.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/text_elider.h"
23 #include "ui/gfx/font.h"
24 #include "ui/gfx/point.h"
28 const int kWindowHeight = 18;
30 // The width of the bubble in relation to the width of the parent window.
31 const CGFloat kWindowWidthPercent = 1.0 / 3.0;
33 // How close the mouse can get to the infobubble before it starts sliding
35 const int kMousePadding = 20;
37 const int kTextPadding = 3;
39 // The animation key used for fade-in and fade-out transitions.
40 NSString* const kFadeAnimationKey = @"alphaValue";
42 // The status bubble's maximum opacity, when fully faded in.
43 const CGFloat kBubbleOpacity = 1.0;
45 // Delay before showing or hiding the bubble after a SetStatus or SetURL call.
46 const int64 kShowDelayMilliseconds = 80;
47 const int64 kHideDelayMilliseconds = 250;
49 // How long each fade should last.
50 const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
51 const NSTimeInterval kHideFadeOutDurationSeconds = 0.200;
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();
59 // How quickly the status bubble should expand, in seconds.
60 const CGFloat kExpansionDuration = 0.125;
64 @interface StatusBubbleAnimationDelegate : NSObject {
66 StatusBubbleMac* statusBubble_; // weak; owns us indirectly
69 - (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble;
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.
76 // CAAnimation delegate method
77 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
80 @implementation StatusBubbleAnimationDelegate
82 - (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble {
83 if ((self = [super init])) {
84 statusBubble_ = statusBubble;
94 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
96 statusBubble_->AnimationDidStop(animation, finished ? true : false);
101 StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
102 : timer_factory_(this),
103 expand_timer_factory_(this),
109 state_(kBubbleHidden),
111 is_expanded_(false) {
116 StatusBubbleMac::~StatusBubbleMac() {
121 [[[window_ animationForKey:kFadeAnimationKey] delegate] invalidate];
127 void StatusBubbleMac::SetStatus(const string16& status) {
128 SetText(status, false);
131 void StatusBubbleMac::SetURL(const GURL& url, const std::string& languages) {
133 languages_ = languages;
135 NSRect frame = [window_ frame];
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];
144 int text_width = static_cast<int>(NSWidth(frame) -
145 kBubbleViewTextPositionX -
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::Font font_chr(base::SysNSStringToUTF8([font fontName]),
156 string16 original_url_text = net::FormatUrl(url, languages);
157 string16 status = gfx::ElideUrl(url, font_chr, text_width, languages);
159 SetText(status, true);
161 // In testing, don't use animation. When ExpandBubble is tested, it is
162 // called explicitly.
168 // If the bubble has been expanded, the user has already hovered over a link
169 // to trigger the expanded state. Don't wait to change the bubble in this
170 // case -- immediately expand or contract to fit the URL.
171 if (is_expanded_ && !url.is_empty()) {
173 } else if (original_url_text.length() > status.length()) {
174 base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
175 base::Bind(&StatusBubbleMac::ExpandBubble,
176 expand_timer_factory_.GetWeakPtr()),
177 base::TimeDelta::FromMilliseconds(kExpandHoverDelay));
181 void StatusBubbleMac::SetText(const string16& text, bool is_url) {
182 // The status bubble allows the status and URL strings to be set
183 // independently. Whichever was set non-empty most recently will be the
184 // value displayed. When both are empty, the status bubble hides.
186 NSString* text_ns = base::SysUTF16ToNSString(text);
193 backup = &status_text_;
195 main = &status_text_;
199 // Don't return from this function early. It's important to make sure that
200 // all calls to StartShowing and StartHiding are made, so that all delays
201 // are observed properly. Specifically, if the state is currently
202 // kBubbleShowingTimer, the timer will need to be restarted even if
203 // [text_ns isEqualToString:*main] is true.
206 *main = [text_ns retain];
209 if ([*main length] > 0)
210 [[window_ contentView] setContent:*main];
211 else if ([*backup length] > 0)
212 [[window_ contentView] setContent:*backup];
217 UpdateSizeAndPosition();
224 void StatusBubbleMac::Hide() {
227 is_expanded_ = false;
229 bool fade_out = false;
230 if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) {
231 SetState(kBubbleHidingFadeOut);
234 // An animation is in progress. Cancel it by starting a new animation.
235 // Use kMinimumTimeInterval to set the opacity as rapidly as possible.
237 [NSAnimationContext beginGrouping];
238 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
239 [[window_ animator] setAlphaValue:0.0];
240 [NSAnimationContext endGrouping];
245 // No animation is in progress, so the opacity can be set directly.
246 [window_ setAlphaValue:0.0];
247 SetState(kBubbleHidden);
250 // Stop any width animation and reset the bubble size.
252 [NSAnimationContext beginGrouping];
253 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
254 [[window_ animator] setFrame:CalculateWindowFrame(/*expand=*/false)
256 [NSAnimationContext endGrouping];
258 [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
261 [status_text_ release];
267 void StatusBubbleMac::SetFrameAvoidingMouse(
268 NSRect window_frame, const gfx::Point& mouse_pos) {
272 // Bubble's base rect in |parent_| (window base) coordinates.
274 if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
275 base_rect = [delegate_ statusBubbleBaseFrame];
277 base_rect = [[parent_ contentView] bounds];
278 base_rect = [[parent_ contentView] convertRect:base_rect toView:nil];
281 // To start, assume default positioning in the lower left corner.
282 // The window_frame position is in global (screen) coordinates.
283 window_frame.origin = [parent_ convertBaseToScreen:base_rect.origin];
285 // Get the cursor position relative to the top right corner of the bubble.
286 gfx::Point relative_pos(mouse_pos.x() - NSMaxX(window_frame),
287 mouse_pos.y() - NSMaxY(window_frame));
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, and
291 // what sorts of corners it should have.
292 unsigned long corner_flags;
293 if (relative_pos.y() < kMousePadding &&
294 relative_pos.x() < kMousePadding) {
295 int offset = kMousePadding - relative_pos.y();
297 // Make the movement non-linear.
298 offset = offset * offset / kMousePadding;
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 (relative_pos.x() > 0) {
303 offset *= (kMousePadding - relative_pos.x()) / kMousePadding;
306 bool is_on_screen = true;
307 NSScreen* screen = [window_ screen];
309 NSMinY([screen visibleFrame]) > NSMinY(window_frame) - offset) {
310 is_on_screen = false;
313 // If something is shown below tab contents (devtools, download shelf etc.),
314 // adjust the position to sit on top of it.
315 bool is_any_shelf_visible = NSMinY(base_rect) > 0;
317 if (is_on_screen && !is_any_shelf_visible) {
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 corner_flags = kRoundedBottomLeftCorner | kRoundedBottomRightCorner;
324 } else if (offset > 0) {
325 corner_flags = kRoundedTopRightCorner |
326 kRoundedBottomLeftCorner |
327 kRoundedBottomRightCorner;
329 corner_flags = kRoundedTopRightCorner;
332 // Place the bubble on the left, but slightly lower.
333 window_frame.origin.y -= offset;
335 // Cannot move the bubble down without obscuring other content.
336 // Move it to the far right instead.
337 corner_flags = kRoundedTopLeftCorner;
338 window_frame.origin.x += NSWidth(base_rect) - NSWidth(window_frame);
341 // Use the default position in the lower left corner of the content area.
342 corner_flags = kRoundedTopRightCorner;
345 corner_flags |= OSDependentCornerFlags(window_frame);
347 [[window_ contentView] setCornerFlags:corner_flags];
348 [window_ setFrame:window_frame display:YES];
351 void StatusBubbleMac::MouseMoved(
352 const gfx::Point& location, bool left_content) {
354 SetFrameAvoidingMouse([window_ frame], location);
357 void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) {
358 UpdateSizeAndPosition();
361 void StatusBubbleMac::Create() {
364 window_ = [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
365 styleMask:NSBorderlessWindowMask
366 backing:NSBackingStoreBuffered
368 [window_ setMovableByWindowBackground:NO];
369 [window_ setBackgroundColor:[NSColor clearColor]];
370 [window_ setLevel:NSNormalWindowLevel];
371 [window_ setOpaque:NO];
372 [window_ setHasShadow:NO];
374 // We do not need to worry about the bubble outliving |parent_| because our
375 // teardown sequence in BWC guarantees that |parent_| outlives the status
376 // bubble and that the StatusBubble is torn down completely prior to the
377 // window going away.
378 base::scoped_nsobject<BubbleView> view(
379 [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]);
380 [window_ setContentView:view];
382 [window_ setAlphaValue:0.0];
384 // TODO(dtseng): Ignore until we provide NSAccessibility support.
385 [window_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
386 forAttribute:NSAccessibilityRoleAttribute];
388 // Set a delegate for the fade-in and fade-out transitions to be notified
389 // when fades are complete. The ownership model is for window_ to own
390 // animation_dictionary, which owns animation, which owns
391 // animation_delegate.
392 CAAnimation* animation = [[window_ animationForKey:kFadeAnimationKey] copy];
393 [animation autorelease];
394 StatusBubbleAnimationDelegate* animation_delegate =
395 [[StatusBubbleAnimationDelegate alloc] initWithStatusBubble:this];
396 [animation_delegate autorelease];
397 [animation setDelegate:animation_delegate];
398 NSMutableDictionary* animation_dictionary =
399 [NSMutableDictionary dictionaryWithDictionary:[window_ animations]];
400 [animation_dictionary setObject:animation forKey:kFadeAnimationKey];
401 [window_ setAnimations:animation_dictionary];
403 [view setCornerFlags:kRoundedTopRightCorner];
404 MouseMoved(gfx::Point(), false);
407 void StatusBubbleMac::Attach() {
408 DCHECK(!is_attached());
410 [window_ orderFront:nil];
411 [parent_ addChildWindow:window_ ordered:NSWindowAbove];
413 [[window_ contentView] setThemeProvider:parent_];
416 void StatusBubbleMac::Detach() {
417 DCHECK(is_attached());
419 // Magic setFrame: See crbug.com/58506, and codereview.chromium.org/3564021
420 [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
421 [parent_ removeChildWindow:window_]; // See crbug.com/28107 ...
422 [window_ orderOut:nil]; // ... and crbug.com/29054.
424 [[window_ contentView] setThemeProvider:nil];
427 void StatusBubbleMac::AnimationDidStop(CAAnimation* animation, bool finished) {
428 DCHECK([NSThread isMainThread]);
429 DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut);
430 DCHECK(is_attached());
433 // Because of the mechanism used to interrupt animations, this is never
434 // actually called with finished set to false. If animations ever become
435 // directly interruptible, the check will ensure that state_ remains
436 // properly synchronized.
437 if (state_ == kBubbleShowingFadeIn) {
438 DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
439 SetState(kBubbleShown);
441 DCHECK_EQ([[window_ animator] alphaValue], 0.0);
442 SetState(kBubbleHidden);
447 void StatusBubbleMac::SetState(StatusBubbleState state) {
451 if (state == kBubbleHidden) {
452 // When hidden (with alpha of 0), make the window have the minimum size,
453 // while still keeping the same origin. It's important to not set the
454 // origin to 0,0 as that will cause the window to use more space in
455 // Expose/Mission Control. See http://crbug.com/81969.
457 // Also, doing it this way instead of detaching the window avoids bugs with
458 // Spaces and Cmd-`. See http://crbug.com/31821 and http://crbug.com/61629.
459 NSRect frame = [window_ frame];
460 frame.size = NSMakeSize(1, 1);
461 [window_ setFrame:frame display:YES];
464 if ([delegate_ respondsToSelector:@selector(statusBubbleWillEnterState:)])
465 [delegate_ statusBubbleWillEnterState:state];
470 void StatusBubbleMac::Fade(bool show) {
471 DCHECK([NSThread isMainThread]);
473 StatusBubbleState fade_state = kBubbleShowingFadeIn;
474 StatusBubbleState target_state = kBubbleShown;
475 NSTimeInterval full_duration = kShowFadeInDurationSeconds;
476 CGFloat opacity = kBubbleOpacity;
479 fade_state = kBubbleHidingFadeOut;
480 target_state = kBubbleHidden;
481 full_duration = kHideFadeOutDurationSeconds;
485 DCHECK(state_ == fade_state || state_ == target_state);
487 if (state_ == target_state)
491 [window_ setAlphaValue:opacity];
492 SetState(target_state);
496 // If an incomplete transition has left the opacity somewhere between 0 and
497 // kBubbleOpacity, the fade rate is kept constant by shortening the duration.
498 NSTimeInterval duration =
500 fabs(opacity - [[window_ animator] alphaValue]) / kBubbleOpacity;
502 // 0.0 will not cancel an in-progress animation.
504 duration = kMinimumTimeInterval;
506 // This will cancel an in-progress transition and replace it with this fade.
507 [NSAnimationContext beginGrouping];
508 // Don't use the GTM additon for the "Steve" slowdown because this can happen
509 // async from user actions and the effects could be a surprise.
510 [[NSAnimationContext currentContext] setDuration:duration];
511 [[window_ animator] setAlphaValue:opacity];
512 [NSAnimationContext endGrouping];
515 void StatusBubbleMac::StartTimer(int64 delay_ms) {
516 DCHECK([NSThread isMainThread]);
517 DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
524 // There can only be one running timer.
527 base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
528 base::Bind(&StatusBubbleMac::TimerFired, timer_factory_.GetWeakPtr()),
529 base::TimeDelta::FromMilliseconds(delay_ms));
532 void StatusBubbleMac::CancelTimer() {
533 DCHECK([NSThread isMainThread]);
535 if (timer_factory_.HasWeakPtrs())
536 timer_factory_.InvalidateWeakPtrs();
539 void StatusBubbleMac::TimerFired() {
540 DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
541 DCHECK([NSThread isMainThread]);
543 if (state_ == kBubbleShowingTimer) {
544 SetState(kBubbleShowingFadeIn);
547 SetState(kBubbleHidingFadeOut);
552 void StatusBubbleMac::StartShowing() {
553 if (state_ == kBubbleHidden) {
554 // Arrange to begin fading in after a delay.
555 SetState(kBubbleShowingTimer);
556 StartTimer(kShowDelayMilliseconds);
557 } else if (state_ == kBubbleHidingFadeOut) {
558 // Cancel the fade-out in progress and replace it with a fade in.
559 SetState(kBubbleShowingFadeIn);
561 } else if (state_ == kBubbleHidingTimer) {
562 // The bubble was already shown but was waiting to begin fading out. It's
563 // given a stay of execution.
564 SetState(kBubbleShown);
566 } else if (state_ == kBubbleShowingTimer) {
567 // The timer was already running but nothing was showing yet. Reaching
568 // this point means that there is a new request to show something. Start
569 // over again by resetting the timer, effectively invalidating the earlier
571 StartTimer(kShowDelayMilliseconds);
574 // If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything
578 void StatusBubbleMac::StartHiding() {
579 if (state_ == kBubbleShown) {
580 // Arrange to begin fading out after a delay.
581 SetState(kBubbleHidingTimer);
582 StartTimer(kHideDelayMilliseconds);
583 } else if (state_ == kBubbleShowingFadeIn) {
584 // Cancel the fade-in in progress and replace it with a fade out.
585 SetState(kBubbleHidingFadeOut);
587 } else if (state_ == kBubbleShowingTimer) {
588 // The bubble was already hidden but was waiting to begin fading in. Too
589 // bad, it won't get the opportunity now.
590 SetState(kBubbleHidden);
594 // If the state is kBubbleHidden, kBubbleHidingFadeOut, or
595 // kBubbleHidingTimer, leave everything alone. The timer is not reset as
596 // with kBubbleShowingTimer in StartShowing() because a subsequent request
597 // to hide something while one is already in flight does not invalidate the
601 void StatusBubbleMac::CancelExpandTimer() {
602 DCHECK([NSThread isMainThread]);
603 expand_timer_factory_.InvalidateWeakPtrs();
606 // Get the current location of the mouse in screen coordinates. To make this
607 // class testable, all code should use this method rather than using
608 // NSEvent mouseLocation directly.
609 gfx::Point StatusBubbleMac::GetMouseLocation() {
610 NSPoint p = [NSEvent mouseLocation];
611 --p.y; // The docs say the y coord starts at 1 not 0; don't ask why.
612 return gfx::Point(p.x, p.y);
615 void StatusBubbleMac::ExpandBubble() {
616 // Calculate the width available for expanded and standard bubbles.
617 NSRect window_frame = CalculateWindowFrame(/*expand=*/true);
618 CGFloat max_bubble_width = NSWidth(window_frame);
619 CGFloat standard_bubble_width =
620 NSWidth(CalculateWindowFrame(/*expand=*/false));
622 // Generate the URL string that fits in the expanded bubble.
623 NSFont* font = [[window_ contentView] font];
624 gfx::Font font_chr(base::SysNSStringToUTF8([font fontName]),
626 string16 expanded_url = gfx::ElideUrl(
627 url_, font_chr, max_bubble_width, languages_);
629 // Scale width from gfx::Font in view coordinates to window coordinates.
630 int required_width_for_string =
631 font_chr.GetStringWidth(expanded_url) +
632 kTextPadding * 2 + kBubbleViewTextPositionX;
633 NSSize scaled_width = NSMakeSize(required_width_for_string, 0);
634 scaled_width = [[parent_ contentView] convertSize:scaled_width toView:nil];
635 required_width_for_string = scaled_width.width;
637 // The expanded width must be at least as wide as the standard width, but no
638 // wider than the maximum width for its parent frame.
639 int expanded_bubble_width =
640 std::max(standard_bubble_width,
641 std::min(max_bubble_width,
642 static_cast<CGFloat>(required_width_for_string)));
644 SetText(expanded_url, true);
646 window_frame.size.width = expanded_bubble_width;
648 // In testing, don't do any animation.
650 [window_ setFrame:window_frame display:YES];
654 NSRect actual_window_frame = [window_ frame];
655 // Adjust status bubble origin if bubble was moved to the right.
656 // TODO(alekseys): fix for RTL.
657 if (NSMinX(actual_window_frame) > NSMinX(window_frame)) {
658 actual_window_frame.origin.x =
659 NSMaxX(actual_window_frame) - NSWidth(window_frame);
661 actual_window_frame.size.width = NSWidth(window_frame);
663 // Do not expand if it's going to cover mouse location.
664 gfx::Point p = GetMouseLocation();
665 if (NSPointInRect(NSMakePoint(p.x(), p.y()), actual_window_frame))
668 // Get the current corner flags and see what needs to change based on the
669 // expansion. This is only needed on Lion, which has rounded window bottoms.
670 if (base::mac::IsOSLionOrLater()) {
671 unsigned long corner_flags = [[window_ contentView] cornerFlags];
672 corner_flags |= OSDependentCornerFlags(actual_window_frame);
673 [[window_ contentView] setCornerFlags:corner_flags];
676 [NSAnimationContext beginGrouping];
677 [[NSAnimationContext currentContext] setDuration:kExpansionDuration];
678 [[window_ animator] setFrame:actual_window_frame display:YES];
679 [NSAnimationContext endGrouping];
682 void StatusBubbleMac::UpdateSizeAndPosition() {
686 SetFrameAvoidingMouse(CalculateWindowFrame(/*expand=*/false),
690 void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) {
692 DCHECK(is_attached());
697 UpdateSizeAndPosition();
700 NSRect StatusBubbleMac::CalculateWindowFrame(bool expanded_width) {
704 if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
705 screenRect = [delegate_ statusBubbleBaseFrame];
706 screenRect.origin = [parent_ convertBaseToScreen:screenRect.origin];
708 screenRect = [parent_ frame];
711 NSSize size = NSMakeSize(0, kWindowHeight);
712 size = [[parent_ contentView] convertSize:size toView:nil];
714 if (expanded_width) {
715 size.width = screenRect.size.width;
717 size.width = kWindowWidthPercent * screenRect.size.width;
720 screenRect.size = size;
724 unsigned long StatusBubbleMac::OSDependentCornerFlags(NSRect window_frame) {
725 unsigned long corner_flags = 0;
727 if (base::mac::IsOSLionOrLater()) {
728 NSRect parent_frame = [parent_ frame];
730 // Round the bottom corners when they're right up against the
731 // corresponding edge of the parent window, or when below the parent
733 if (NSMinY(window_frame) <= NSMinY(parent_frame)) {
734 if (NSMinX(window_frame) == NSMinX(parent_frame)) {
735 corner_flags |= kRoundedBottomLeftCorner;
738 if (NSMaxX(window_frame) == NSMaxX(parent_frame)) {
739 corner_flags |= kRoundedBottomRightCorner;
743 // Round the top corners when the bubble is below the parent window.
744 if (NSMinY(window_frame) < NSMinY(parent_frame)) {
745 corner_flags |= kRoundedTopLeftCorner | kRoundedTopRightCorner;