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