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#include "chrome/browser/themes/theme_properties.h" 12#include "chrome/browser/themes/theme_service.h" 13#import "chrome/browser/ui/cocoa/browser_window_controller.h" 14#import "chrome/browser/ui/cocoa/browser_window_utils.h" 15#import "chrome/browser/ui/cocoa/custom_frame_view.h" 16#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h" 17#import "chrome/browser/ui/cocoa/themed_window.h" 18#include "grit/theme_resources.h" 19#include "ui/base/cocoa/nsgraphics_context_additions.h" 20#import "ui/base/cocoa/nsview_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 (Private) 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 shouldHideTitle_ = flag; 256} 257 258- (BOOL)_isTitleHidden { 259 return shouldHideTitle_; 260} 261 262- (CGFloat)windowButtonsInterButtonSpacing { 263 return windowButtonsInterButtonSpacing_; 264} 265 266// This method is called whenever a window is moved in order to ensure it fits 267// on the screen. We cannot always handle resizes without breaking, so we 268// prevent frame constraining in those cases. 269- (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen { 270 // Do not constrain the frame rect if our delegate says no. In this case, 271 // return the original (unconstrained) frame. 272 id delegate = [self delegate]; 273 if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] && 274 ![delegate shouldConstrainFrameRect]) 275 return frame; 276 277 return [super constrainFrameRect:frame toScreen:screen]; 278} 279 280// This method is overridden in order to send the toggle fullscreen message 281// through the cross-platform browser framework before going fullscreen. The 282// message will eventually come back as a call to |-toggleSystemFullScreen|, 283// which in turn calls AppKit's |NSWindow -toggleFullScreen:|. 284- (void)toggleFullScreen:(id)sender { 285 id delegate = [self delegate]; 286 if ([delegate respondsToSelector:@selector(handleLionToggleFullscreen)]) 287 [delegate handleLionToggleFullscreen]; 288} 289 290- (void)toggleSystemFullScreen { 291 if ([super respondsToSelector:@selector(toggleFullScreen:)]) 292 [super toggleFullScreen:nil]; 293} 294 295- (NSPoint)fullScreenButtonOriginAdjustment { 296 if (!hasTabStrip_) 297 return NSZeroPoint; 298 299 // Vertically center the button. 300 NSPoint origin = NSMakePoint(0, -6); 301 302 // If there is a profile avatar icon present, shift the button over by its 303 // width and some padding. The new avatar button is displayed to the right 304 // of the fullscreen icon, so it doesn't need to be shifted. 305 BrowserWindowController* bwc = 306 static_cast<BrowserWindowController*>([self windowController]); 307 if ([bwc shouldShowAvatar] && ![bwc shouldUseNewAvatarButton]) { 308 NSView* avatarButton = [[bwc avatarButtonController] view]; 309 origin.x = -(NSWidth([avatarButton frame]) + 3); 310 } else { 311 origin.x -= 6; 312 } 313 314 return origin; 315} 316 317- (void)drawCustomFrameRect:(NSRect)rect forView:(NSView*)view { 318 // WARNING: There is an obvious optimization opportunity here that you DO NOT 319 // want to take. To save painting cycles, you might think it would be a good 320 // idea to call out to the default implementation only if no theme were 321 // drawn. In reality, however, if you fail to call the default 322 // implementation, or if you call it after a clipping path is set, the 323 // rounded corners at the top of the window will not draw properly. Do not 324 // try to be smart here. 325 326 // Only paint the top of the window. 327 NSRect windowRect = [view convertRect:[self frame] fromView:nil]; 328 windowRect.origin = NSZeroPoint; 329 330 NSRect paintRect = windowRect; 331 paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight; 332 paintRect.size.height = kBrowserFrameViewPaintHeight; 333 rect = NSIntersectionRect(paintRect, rect); 334 [super drawCustomFrameRect:rect forView:view]; 335 336 // Set up our clip. 337 float cornerRadius = 4.0; 338 if ([view respondsToSelector:@selector(roundedCornerRadius)]) 339 cornerRadius = [view roundedCornerRadius]; 340 [[NSBezierPath bezierPathWithRoundedRect:windowRect 341 xRadius:cornerRadius 342 yRadius:cornerRadius] addClip]; 343 [[NSBezierPath bezierPathWithRect:rect] addClip]; 344 345 // Do the theming. 346 BOOL themed = [FramedBrowserWindow 347 drawWindowThemeInDirtyRect:rect 348 forView:view 349 bounds:windowRect 350 forceBlackBackground:NO]; 351 352 // If the window needs a title and we painted over the title as drawn by the 353 // default window paint, paint it ourselves. 354 if (themed && [view respondsToSelector:@selector(_titlebarTitleRect)] && 355 [view respondsToSelector:@selector(_drawTitleStringIn:withColor:)] && 356 ![self _isTitleHidden]) { 357 [view _drawTitleStringIn:[view _titlebarTitleRect] 358 withColor:[self titleColor]]; 359 } 360 361 // Pinstripe the top. 362 if (themed) { 363 CGFloat lineWidth = [view cr_lineWidth]; 364 365 windowRect = [view convertRect:[self frame] fromView:nil]; 366 windowRect.origin = NSZeroPoint; 367 windowRect.origin.y -= 0.5 * lineWidth; 368 windowRect.origin.x -= 0.5 * lineWidth; 369 windowRect.size.width += lineWidth; 370 [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set]; 371 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect 372 xRadius:cornerRadius 373 yRadius:cornerRadius]; 374 [path setLineWidth:lineWidth]; 375 [path stroke]; 376 } 377} 378 379+ (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect 380 forView:(NSView*)view 381 bounds:(NSRect)bounds 382 forceBlackBackground:(BOOL)forceBlackBackground { 383 ui::ThemeProvider* themeProvider = [[view window] themeProvider]; 384 if (!themeProvider) 385 return NO; 386 387 ThemedWindowStyle windowStyle = [[view window] themedWindowStyle]; 388 389 // Devtools windows don't get themed. 390 if (windowStyle & THEMED_DEVTOOLS) 391 return NO; 392 393 BOOL active = [[view window] isMainWindow]; 394 BOOL incognito = windowStyle & THEMED_INCOGNITO; 395 BOOL popup = windowStyle & THEMED_POPUP; 396 397 // Find a theme image. 398 NSColor* themeImageColor = nil; 399 if (!popup) { 400 int themeImageID; 401 if (active && incognito) 402 themeImageID = IDR_THEME_FRAME_INCOGNITO; 403 else if (active && !incognito) 404 themeImageID = IDR_THEME_FRAME; 405 else if (!active && incognito) 406 themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE; 407 else 408 themeImageID = IDR_THEME_FRAME_INACTIVE; 409 if (themeProvider->HasCustomImage(IDR_THEME_FRAME)) 410 themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID); 411 } 412 413 // If no theme image, use a gradient if incognito. 414 NSGradient* gradient = nil; 415 if (!themeImageColor && incognito) 416 gradient = themeProvider->GetNSGradient( 417 active ? ThemeProperties::GRADIENT_FRAME_INCOGNITO : 418 ThemeProperties::GRADIENT_FRAME_INCOGNITO_INACTIVE); 419 420 BOOL themed = NO; 421 if (themeImageColor) { 422 // Default to replacing any existing pixels with the theme image, but if 423 // asked paint black first and blend the theme with black. 424 NSCompositingOperation operation = NSCompositeCopy; 425 if (forceBlackBackground) { 426 [[NSColor blackColor] set]; 427 NSRectFill(dirtyRect); 428 operation = NSCompositeSourceOver; 429 } 430 431 NSPoint position = [[view window] themeImagePositionForAlignment: 432 THEME_IMAGE_ALIGN_WITH_FRAME]; 433 434 // Align the phase to physical pixels so resizing the window under HiDPI 435 // doesn't cause wiggling of the theme. 436 NSView* frameView = [[[view window] contentView] superview]; 437 position = [frameView convertPointToBase:position]; 438 position.x = floor(position.x); 439 position.y = floor(position.y); 440 position = [frameView convertPointFromBase:position]; 441 [[NSGraphicsContext currentContext] cr_setPatternPhase:position 442 forView:view]; 443 444 [themeImageColor set]; 445 NSRectFillUsingOperation(dirtyRect, operation); 446 themed = YES; 447 } else if (gradient) { 448 NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds)); 449 NSPoint endPoint = startPoint; 450 endPoint.y -= kBrowserFrameViewPaintHeight; 451 [gradient drawFromPoint:startPoint toPoint:endPoint options:0]; 452 themed = YES; 453 } 454 455 // Check to see if we have an overlay image. 456 NSImage* overlayImage = nil; 457 if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && !incognito && 458 !popup) { 459 overlayImage = themeProvider-> 460 GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY : 461 IDR_THEME_FRAME_OVERLAY_INACTIVE); 462 } 463 464 if (overlayImage) { 465 // Anchor to top-left and don't scale. 466 NSView* frameView = [[[view window] contentView] superview]; 467 NSPoint position = [[view window] themeImagePositionForAlignment: 468 THEME_IMAGE_ALIGN_WITH_FRAME]; 469 position = [view convertPoint:position fromView:frameView]; 470 NSSize overlaySize = [overlayImage size]; 471 NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height); 472 [overlayImage drawAtPoint:NSMakePoint(position.x, 473 position.y - overlaySize.height) 474 fromRect:imageFrame 475 operation:NSCompositeSourceOver 476 fraction:1.0]; 477 } 478 479 return themed; 480} 481 482- (NSColor*)titleColor { 483 ui::ThemeProvider* themeProvider = [self themeProvider]; 484 if (!themeProvider) 485 return [NSColor windowFrameTextColor]; 486 487 ThemedWindowStyle windowStyle = [self themedWindowStyle]; 488 BOOL incognito = windowStyle & THEMED_INCOGNITO; 489 490 if (incognito) 491 return [NSColor whiteColor]; 492 else 493 return [NSColor windowFrameTextColor]; 494} 495 496@end 497