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 #import "chrome/browser/ui/cocoa/framed_browser_window.h" 6 7 #include "base/logging.h" 8 #include "base/mac/sdk_forward_declarations.h" 9 #include "chrome/browser/global_keyboard_shortcuts_mac.h" 10 #include "chrome/browser/profiles/profile_avatar_icon_util.h" 11 #import "chrome/browser/ui/cocoa/browser_window_controller.h" 12 #import "chrome/browser/ui/cocoa/browser_window_utils.h" 13 #import "chrome/browser/ui/cocoa/custom_frame_view.h" 14 #import "chrome/browser/ui/cocoa/nsview_additions.h" 15 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h" 16 #import "chrome/browser/ui/cocoa/themed_window.h" 17 #include "chrome/browser/themes/theme_properties.h" 18 #include "chrome/browser/themes/theme_service.h" 19 #include "grit/theme_resources.h" 20 #include "ui/base/cocoa/nsgraphics_context_additions.h" 21 22 // Implementer's note: Moving the window controls is tricky. When altering the 23 // code, ensure that: 24 // - accessibility hit testing works 25 // - the accessibility hierarchy is correct 26 // - close/min in the background don't bring the window forward 27 // - rollover effects work correctly 28 29 namespace { 30 31 const CGFloat kBrowserFrameViewPaintHeight = 60.0; 32 33 // Size of the gradient. Empirically determined so that the gradient looks 34 // like what the heuristic does when there are just a few tabs. 35 const CGFloat kWindowGradientHeight = 24.0; 36 37 } 38 39 @interface FramedBrowserWindow () 40 41 - (void)adjustCloseButton:(NSNotification*)notification; 42 - (void)adjustMiniaturizeButton:(NSNotification*)notification; 43 - (void)adjustZoomButton:(NSNotification*)notification; 44 - (void)adjustButton:(NSButton*)button 45 ofKind:(NSWindowButton)kind; 46 - (NSView*)frameView; 47 48 @end 49 50 // Undocumented APIs. They are really on NSGrayFrame rather than NSView. Take 51 // care to only call them on the NSView passed into 52 // -[NSWindow drawCustomRect:forView:]. 53 @interface NSView (UndocumentedAPI) 54 55 - (float)roundedCornerRadius; 56 - (CGRect)_titlebarTitleRect; 57 - (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color; 58 59 @end 60 61 62 @implementation FramedBrowserWindow 63 64 - (id)initWithContentRect:(NSRect)contentRect 65 hasTabStrip:(BOOL)hasTabStrip{ 66 NSUInteger styleMask = NSTitledWindowMask | 67 NSClosableWindowMask | 68 NSMiniaturizableWindowMask | 69 NSResizableWindowMask | 70 NSTexturedBackgroundWindowMask; 71 if ((self = [super initWithContentRect:contentRect 72 styleMask:styleMask 73 backing:NSBackingStoreBuffered 74 defer:YES])) { 75 // The 10.6 fullscreen code copies the title to a different window, which 76 // will assert if it's nil. 77 [self setTitle:@""]; 78 79 // The following two calls fix http://crbug.com/25684 by preventing the 80 // window from recalculating the border thickness as the window is 81 // resized. 82 // This was causing the window tint to change for the default system theme 83 // when the window was being resized. 84 [self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge]; 85 [self setContentBorderThickness:kWindowGradientHeight forEdge:NSMaxYEdge]; 86 87 hasTabStrip_ = hasTabStrip; 88 closeButton_ = [self standardWindowButton:NSWindowCloseButton]; 89 [closeButton_ setPostsFrameChangedNotifications:YES]; 90 miniaturizeButton_ = [self standardWindowButton:NSWindowMiniaturizeButton]; 91 [miniaturizeButton_ setPostsFrameChangedNotifications:YES]; 92 zoomButton_ = [self standardWindowButton:NSWindowZoomButton]; 93 [zoomButton_ setPostsFrameChangedNotifications:YES]; 94 95 windowButtonsInterButtonSpacing_ = 96 NSMinX([miniaturizeButton_ frame]) - NSMaxX([closeButton_ frame]); 97 98 [self adjustButton:closeButton_ ofKind:NSWindowCloseButton]; 99 [self adjustButton:miniaturizeButton_ ofKind:NSWindowMiniaturizeButton]; 100 [self adjustButton:zoomButton_ ofKind:NSWindowZoomButton]; 101 102 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 103 [center addObserver:self 104 selector:@selector(adjustCloseButton:) 105 name:NSViewFrameDidChangeNotification 106 object:closeButton_]; 107 [center addObserver:self 108 selector:@selector(adjustMiniaturizeButton:) 109 name:NSViewFrameDidChangeNotification 110 object:miniaturizeButton_]; 111 [center addObserver:self 112 selector:@selector(adjustZoomButton:) 113 name:NSViewFrameDidChangeNotification 114 object:zoomButton_]; 115 [center addObserver:self 116 selector:@selector(themeDidChangeNotification:) 117 name:kBrowserThemeDidChangeNotification 118 object:nil]; 119 } 120 121 return self; 122 } 123 124 - (void)dealloc { 125 [[NSNotificationCenter defaultCenter] removeObserver:self]; 126 [super dealloc]; 127 } 128 129 - (void)adjustCloseButton:(NSNotification*)notification { 130 [self adjustButton:[notification object] 131 ofKind:NSWindowCloseButton]; 132 } 133 134 - (void)adjustMiniaturizeButton:(NSNotification*)notification { 135 [self adjustButton:[notification object] 136 ofKind:NSWindowMiniaturizeButton]; 137 } 138 139 - (void)adjustZoomButton:(NSNotification*)notification { 140 [self adjustButton:[notification object] 141 ofKind:NSWindowZoomButton]; 142 } 143 144 - (void)adjustButton:(NSButton*)button 145 ofKind:(NSWindowButton)kind { 146 NSRect buttonFrame = [button frame]; 147 NSRect frameViewBounds = [[self frameView] bounds]; 148 149 CGFloat xOffset = hasTabStrip_ 150 ? kFramedWindowButtonsWithTabStripOffsetFromLeft 151 : kFramedWindowButtonsWithoutTabStripOffsetFromLeft; 152 CGFloat yOffset = hasTabStrip_ 153 ? kFramedWindowButtonsWithTabStripOffsetFromTop 154 : kFramedWindowButtonsWithoutTabStripOffsetFromTop; 155 buttonFrame.origin = 156 NSMakePoint(xOffset, (NSHeight(frameViewBounds) - 157 NSHeight(buttonFrame) - yOffset)); 158 159 switch (kind) { 160 case NSWindowZoomButton: 161 buttonFrame.origin.x += NSWidth([miniaturizeButton_ frame]); 162 buttonFrame.origin.x += windowButtonsInterButtonSpacing_; 163 // fallthrough 164 case NSWindowMiniaturizeButton: 165 buttonFrame.origin.x += NSWidth([closeButton_ frame]); 166 buttonFrame.origin.x += windowButtonsInterButtonSpacing_; 167 // fallthrough 168 default: 169 break; 170 } 171 172 BOOL didPost = [button postsBoundsChangedNotifications]; 173 [button setPostsFrameChangedNotifications:NO]; 174 [button setFrame:buttonFrame]; 175 [button setPostsFrameChangedNotifications:didPost]; 176 } 177 178 - (NSView*)frameView { 179 return [[self contentView] superview]; 180 } 181 182 // The tab strip view covers our window buttons. So we add hit testing here 183 // to find them properly and return them to the accessibility system. 184 - (id)accessibilityHitTest:(NSPoint)point { 185 NSPoint windowPoint = [self convertScreenToBase:point]; 186 NSControl* controls[] = { closeButton_, zoomButton_, miniaturizeButton_ }; 187 id value = nil; 188 for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); ++i) { 189 if (NSPointInRect(windowPoint, [controls[i] frame])) { 190 value = [controls[i] accessibilityHitTest:point]; 191 break; 192 } 193 } 194 if (!value) { 195 value = [super accessibilityHitTest:point]; 196 } 197 return value; 198 } 199 200 - (void)windowMainStatusChanged { 201 NSView* frameView = [self frameView]; 202 NSView* contentView = [self contentView]; 203 NSRect updateRect = [frameView frame]; 204 NSRect contentRect = [contentView frame]; 205 CGFloat tabStripHeight = [TabStripController defaultTabHeight]; 206 updateRect.size.height -= NSHeight(contentRect) - tabStripHeight; 207 updateRect.origin.y = NSMaxY(contentRect) - tabStripHeight; 208 [[self frameView] setNeedsDisplayInRect:updateRect]; 209 } 210 211 - (void)becomeMainWindow { 212 [self windowMainStatusChanged]; 213 [super becomeMainWindow]; 214 } 215 216 - (void)resignMainWindow { 217 [self windowMainStatusChanged]; 218 [super resignMainWindow]; 219 } 220 221 // Called after the current theme has changed. 222 - (void)themeDidChangeNotification:(NSNotification*)aNotification { 223 [[self frameView] setNeedsDisplay:YES]; 224 } 225 226 - (void)sendEvent:(NSEvent*)event { 227 // For Cocoa windows, clicking on the close and the miniaturize buttons (but 228 // not the zoom button) while a window is in the background does NOT bring 229 // that window to the front. We don't get that behavior for free (probably 230 // because the tab strip view covers those buttons), so we handle it here. 231 // Zoom buttons do bring the window to the front. Note that Finder windows (in 232 // Leopard) behave differently in this regard in that zoom buttons don't bring 233 // the window to the foreground. 234 BOOL eventHandled = NO; 235 if (![self isMainWindow]) { 236 if ([event type] == NSLeftMouseDown) { 237 NSView* frameView = [self frameView]; 238 NSPoint mouse = [frameView convertPoint:[event locationInWindow] 239 fromView:nil]; 240 if (NSPointInRect(mouse, [closeButton_ frame])) { 241 [closeButton_ mouseDown:event]; 242 eventHandled = YES; 243 } else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) { 244 [miniaturizeButton_ mouseDown:event]; 245 eventHandled = YES; 246 } 247 } 248 } 249 if (!eventHandled) { 250 [super sendEvent:event]; 251 } 252 } 253 254 - (void)setShouldHideTitle:(BOOL)flag { 255 if ([self respondsToSelector:@selector(setTitleVisibility:)]) 256 self.titleVisibility = flag ? NSWindowTitleHidden : NSWindowTitleVisible; 257 else 258 shouldHideTitle_ = flag; 259 } 260 261 - (BOOL)_isTitleHidden { 262 // Only intervene with 10.6-10.9. 263 if ([self respondsToSelector:@selector(setTitleVisibility:)]) 264 return [super _isTitleHidden]; 265 else 266 return shouldHideTitle_; 267 } 268 269 - (CGFloat)windowButtonsInterButtonSpacing { 270 return windowButtonsInterButtonSpacing_; 271 } 272 273 // This method is called whenever a window is moved in order to ensure it fits 274 // on the screen. We cannot always handle resizes without breaking, so we 275 // prevent frame constraining in those cases. 276 - (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen { 277 // Do not constrain the frame rect if our delegate says no. In this case, 278 // return the original (unconstrained) frame. 279 id delegate = [self delegate]; 280 if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] && 281 ![delegate shouldConstrainFrameRect]) 282 return frame; 283 284 return [super constrainFrameRect:frame toScreen:screen]; 285 } 286 287 // This method is overridden in order to send the toggle fullscreen message 288 // through the cross-platform browser framework before going fullscreen. The 289 // message will eventually come back as a call to |-toggleSystemFullScreen|, 290 // which in turn calls AppKit's |NSWindow -toggleFullScreen:|. 291 - (void)toggleFullScreen:(id)sender { 292 id delegate = [self delegate]; 293 if ([delegate respondsToSelector:@selector(handleLionToggleFullscreen)]) 294 [delegate handleLionToggleFullscreen]; 295 } 296 297 - (void)toggleSystemFullScreen { 298 if ([super respondsToSelector:@selector(toggleFullScreen:)]) 299 [super toggleFullScreen:nil]; 300 } 301 302 - (NSPoint)fullScreenButtonOriginAdjustment { 303 if (!hasTabStrip_) 304 return NSZeroPoint; 305 306 // Vertically center the button. 307 NSPoint origin = NSMakePoint(0, -6); 308 309 // If there is a profile avatar icon present, shift the button over by its 310 // width and some padding. The new avatar button is displayed to the right 311 // of the fullscreen icon, so it doesn't need to be shifted. 312 BrowserWindowController* bwc = 313 static_cast<BrowserWindowController*>([self windowController]); 314 if ([bwc shouldShowAvatar] && ![bwc shouldUseNewAvatarButton]) { 315 NSView* avatarButton = [[bwc avatarButtonController] view]; 316 origin.x = -(NSWidth([avatarButton frame]) + 3); 317 } else { 318 origin.x -= 6; 319 } 320 321 return origin; 322 } 323 324 - (void)drawCustomFrameRect:(NSRect)rect forView:(NSView*)view { 325 // WARNING: There is an obvious optimization opportunity here that you DO NOT 326 // want to take. To save painting cycles, you might think it would be a good 327 // idea to call out to the default implementation only if no theme were 328 // drawn. In reality, however, if you fail to call the default 329 // implementation, or if you call it after a clipping path is set, the 330 // rounded corners at the top of the window will not draw properly. Do not 331 // try to be smart here. 332 333 // Only paint the top of the window. 334 NSRect windowRect = [view convertRect:[self frame] fromView:nil]; 335 windowRect.origin = NSZeroPoint; 336 337 NSRect paintRect = windowRect; 338 paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight; 339 paintRect.size.height = kBrowserFrameViewPaintHeight; 340 rect = NSIntersectionRect(paintRect, rect); 341 [super drawCustomFrameRect:rect forView:view]; 342 343 // Set up our clip. 344 float cornerRadius = 4.0; 345 if ([view respondsToSelector:@selector(roundedCornerRadius)]) 346 cornerRadius = [view roundedCornerRadius]; 347 [[NSBezierPath bezierPathWithRoundedRect:windowRect 348 xRadius:cornerRadius 349 yRadius:cornerRadius] addClip]; 350 [[NSBezierPath bezierPathWithRect:rect] addClip]; 351 352 // Do the theming. 353 BOOL themed = [FramedBrowserWindow 354 drawWindowThemeInDirtyRect:rect 355 forView:view 356 bounds:windowRect 357 forceBlackBackground:NO]; 358 359 // In Yosemite: The title is drawn by a subview and not painted on. Therefore, 360 // never worry about drawing it. Pre-Yosemite: If the window needs a title and 361 // we painted over the title as drawn by the default window paint, paint it 362 // ourselves. 363 if (![self respondsToSelector:@selector(setTitleVisibility:)] && 364 themed && [view respondsToSelector:@selector(_titlebarTitleRect)] && 365 [view respondsToSelector:@selector(_drawTitleStringIn:withColor:)] && 366 ![self _isTitleHidden]) { 367 [view _drawTitleStringIn:[view _titlebarTitleRect] 368 withColor:[self titleColor]]; 369 } 370 371 // Pinstripe the top. 372 if (themed) { 373 CGFloat lineWidth = [view cr_lineWidth]; 374 375 windowRect = [view convertRect:[self frame] fromView:nil]; 376 windowRect.origin = NSZeroPoint; 377 windowRect.origin.y -= 0.5 * lineWidth; 378 windowRect.origin.x -= 0.5 * lineWidth; 379 windowRect.size.width += lineWidth; 380 [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set]; 381 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect 382 xRadius:cornerRadius 383 yRadius:cornerRadius]; 384 [path setLineWidth:lineWidth]; 385 [path stroke]; 386 } 387 } 388 389 + (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect 390 forView:(NSView*)view 391 bounds:(NSRect)bounds 392 forceBlackBackground:(BOOL)forceBlackBackground { 393 ui::ThemeProvider* themeProvider = [[view window] themeProvider]; 394 if (!themeProvider) 395 return NO; 396 397 ThemedWindowStyle windowStyle = [[view window] themedWindowStyle]; 398 399 // Devtools windows don't get themed. 400 if (windowStyle & THEMED_DEVTOOLS) 401 return NO; 402 403 BOOL active = [[view window] isMainWindow]; 404 BOOL incognito = windowStyle & THEMED_INCOGNITO; 405 BOOL popup = windowStyle & THEMED_POPUP; 406 407 // Find a theme image. 408 NSColor* themeImageColor = nil; 409 if (!popup) { 410 int themeImageID; 411 if (active && incognito) 412 themeImageID = IDR_THEME_FRAME_INCOGNITO; 413 else if (active && !incognito) 414 themeImageID = IDR_THEME_FRAME; 415 else if (!active && incognito) 416 themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE; 417 else 418 themeImageID = IDR_THEME_FRAME_INACTIVE; 419 if (themeProvider->HasCustomImage(IDR_THEME_FRAME)) 420 themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID); 421 } 422 423 // If no theme image, use a gradient if incognito. 424 NSGradient* gradient = nil; 425 if (!themeImageColor && incognito) 426 gradient = themeProvider->GetNSGradient( 427 active ? ThemeProperties::GRADIENT_FRAME_INCOGNITO : 428 ThemeProperties::GRADIENT_FRAME_INCOGNITO_INACTIVE); 429 430 BOOL themed = NO; 431 if (themeImageColor) { 432 // Default to replacing any existing pixels with the theme image, but if 433 // asked paint black first and blend the theme with black. 434 NSCompositingOperation operation = NSCompositeCopy; 435 if (forceBlackBackground) { 436 [[NSColor blackColor] set]; 437 NSRectFill(dirtyRect); 438 operation = NSCompositeSourceOver; 439 } 440 441 NSPoint position = [[view window] themeImagePositionForAlignment: 442 THEME_IMAGE_ALIGN_WITH_FRAME]; 443 444 // Align the phase to physical pixels so resizing the window under HiDPI 445 // doesn't cause wiggling of the theme. 446 NSView* frameView = [[[view window] contentView] superview]; 447 position = [frameView convertPointToBase:position]; 448 position.x = floor(position.x); 449 position.y = floor(position.y); 450 position = [frameView convertPointFromBase:position]; 451 [[NSGraphicsContext currentContext] cr_setPatternPhase:position 452 forView:view]; 453 454 [themeImageColor set]; 455 NSRectFillUsingOperation(dirtyRect, operation); 456 themed = YES; 457 } else if (gradient) { 458 NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds)); 459 NSPoint endPoint = startPoint; 460 endPoint.y -= kBrowserFrameViewPaintHeight; 461 [gradient drawFromPoint:startPoint toPoint:endPoint options:0]; 462 themed = YES; 463 } 464 465 // Check to see if we have an overlay image. 466 NSImage* overlayImage = nil; 467 if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && !incognito && 468 !popup) { 469 overlayImage = themeProvider-> 470 GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY : 471 IDR_THEME_FRAME_OVERLAY_INACTIVE); 472 } 473 474 if (overlayImage) { 475 // Anchor to top-left and don't scale. 476 NSView* frameView = [[[view window] contentView] superview]; 477 NSPoint position = [[view window] themeImagePositionForAlignment: 478 THEME_IMAGE_ALIGN_WITH_FRAME]; 479 position = [view convertPoint:position fromView:frameView]; 480 NSSize overlaySize = [overlayImage size]; 481 NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height); 482 [overlayImage drawAtPoint:NSMakePoint(position.x, 483 position.y - overlaySize.height) 484 fromRect:imageFrame 485 operation:NSCompositeSourceOver 486 fraction:1.0]; 487 } 488 489 return themed; 490 } 491 492 - (NSColor*)titleColor { 493 ui::ThemeProvider* themeProvider = [self themeProvider]; 494 if (!themeProvider) 495 return [NSColor windowFrameTextColor]; 496 497 ThemedWindowStyle windowStyle = [self themedWindowStyle]; 498 BOOL incognito = windowStyle & THEMED_INCOGNITO; 499 500 if (incognito) 501 return [NSColor whiteColor]; 502 else 503 return [NSColor windowFrameTextColor]; 504 } 505 506 @end 507