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