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 29namespace { 30 31const 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. 35const 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