1 // Copyright (c) 2011 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/browser_frame_view.h" 6 7 #import <objc/runtime.h> 8 #import <Carbon/Carbon.h> 9 10 #include "base/logging.h" 11 #include "base/mac/scoped_nsautorelease_pool.h" 12 #import "chrome/browser/themes/theme_service.h" 13 #import "chrome/browser/ui/cocoa/framed_browser_window.h" 14 #import "chrome/browser/ui/cocoa/themed_window.h" 15 #include "grit/theme_resources.h" 16 17 static const CGFloat kBrowserFrameViewPaintHeight = 60.0; 18 static const NSPoint kBrowserFrameViewPatternPhaseOffset = { -5, 3 }; 19 20 static BOOL gCanDrawTitle = NO; 21 static BOOL gCanGetCornerRadius = NO; 22 23 @interface NSView (Swizzles) 24 - (void)drawRectOriginal:(NSRect)rect; 25 - (NSUInteger)_shadowFlagsOriginal; 26 @end 27 28 // Undocumented APIs. They are really on NSGrayFrame rather than 29 // BrowserFrameView, but we call them from methods swizzled onto NSGrayFrame. 30 @interface BrowserFrameView (UndocumentedAPI) 31 32 - (float)roundedCornerRadius; 33 - (CGRect)_titlebarTitleRect; 34 - (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color; 35 - (NSUInteger)_shadowFlags; 36 37 @end 38 39 @implementation BrowserFrameView 40 41 + (void)load { 42 // This is where we swizzle drawRect, and add in two methods that we 43 // need. If any of these fail it shouldn't affect the functionality of the 44 // others. If they all fail, we will lose window frame theming and 45 // roll overs for our close widgets, but things should still function 46 // correctly. 47 base::mac::ScopedNSAutoreleasePool pool; 48 Class grayFrameClass = NSClassFromString(@"NSGrayFrame"); 49 DCHECK(grayFrameClass); 50 if (!grayFrameClass) return; 51 52 // Exchange draw rect. 53 Method m0 = class_getInstanceMethod([self class], @selector(drawRect:)); 54 DCHECK(m0); 55 if (m0) { 56 BOOL didAdd = class_addMethod(grayFrameClass, 57 @selector(drawRectOriginal:), 58 method_getImplementation(m0), 59 method_getTypeEncoding(m0)); 60 DCHECK(didAdd); 61 if (didAdd) { 62 Method m1 = class_getInstanceMethod(grayFrameClass, @selector(drawRect:)); 63 Method m2 = class_getInstanceMethod(grayFrameClass, 64 @selector(drawRectOriginal:)); 65 DCHECK(m1 && m2); 66 if (m1 && m2) { 67 method_exchangeImplementations(m1, m2); 68 } 69 } 70 } 71 72 gCanDrawTitle = 73 [grayFrameClass 74 instancesRespondToSelector:@selector(_titlebarTitleRect)] && 75 [grayFrameClass 76 instancesRespondToSelector:@selector(_drawTitleStringIn:withColor:)]; 77 gCanGetCornerRadius = 78 [grayFrameClass 79 instancesRespondToSelector:@selector(roundedCornerRadius)]; 80 81 // Add _shadowFlags. This is a method on NSThemeFrame, not on NSGrayFrame. 82 // NSThemeFrame is NSGrayFrame's superclass. 83 Class themeFrameClass = NSClassFromString(@"NSThemeFrame"); 84 DCHECK(themeFrameClass); 85 if (!themeFrameClass) return; 86 m0 = class_getInstanceMethod([self class], @selector(_shadowFlags)); 87 DCHECK(m0); 88 if (m0) { 89 BOOL didAdd = class_addMethod(themeFrameClass, 90 @selector(_shadowFlagsOriginal), 91 method_getImplementation(m0), 92 method_getTypeEncoding(m0)); 93 DCHECK(didAdd); 94 if (didAdd) { 95 Method m1 = class_getInstanceMethod(themeFrameClass, 96 @selector(_shadowFlags)); 97 Method m2 = class_getInstanceMethod(themeFrameClass, 98 @selector(_shadowFlagsOriginal)); 99 DCHECK(m1 && m2); 100 if (m1 && m2) { 101 method_exchangeImplementations(m1, m2); 102 } 103 } 104 } 105 } 106 107 - (id)initWithFrame:(NSRect)frame { 108 // This class is not for instantiating. 109 [self doesNotRecognizeSelector:_cmd]; 110 return nil; 111 } 112 113 - (id)initWithCoder:(NSCoder*)coder { 114 // This class is not for instantiating. 115 [self doesNotRecognizeSelector:_cmd]; 116 return nil; 117 } 118 119 // Here is our custom drawing for our frame. 120 - (void)drawRect:(NSRect)rect { 121 // If this isn't the window class we expect, then pass it on to the 122 // original implementation. 123 if (![[self window] isKindOfClass:[FramedBrowserWindow class]]) { 124 [self drawRectOriginal:rect]; 125 return; 126 } 127 128 // WARNING: There is an obvious optimization opportunity here that you DO NOT 129 // want to take. To save painting cycles, you might think it would be a good 130 // idea to call out to -drawRectOriginal: only if no theme were drawn. In 131 // reality, however, if you fail to call -drawRectOriginal:, or if you call it 132 // after a clipping path is set, the rounded corners at the top of the window 133 // will not draw properly. Do not try to be smart here. 134 135 // Only paint the top of the window. 136 NSWindow* window = [self window]; 137 NSRect windowRect = [self convertRect:[window frame] fromView:nil]; 138 windowRect.origin = NSMakePoint(0, 0); 139 140 NSRect paintRect = windowRect; 141 paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight; 142 paintRect.size.height = kBrowserFrameViewPaintHeight; 143 rect = NSIntersectionRect(paintRect, rect); 144 [self drawRectOriginal:rect]; 145 146 // Set up our clip. 147 float cornerRadius = 4.0; 148 if (gCanGetCornerRadius) 149 cornerRadius = [self roundedCornerRadius]; 150 [[NSBezierPath bezierPathWithRoundedRect:windowRect 151 xRadius:cornerRadius 152 yRadius:cornerRadius] addClip]; 153 [[NSBezierPath bezierPathWithRect:rect] addClip]; 154 155 // Do the theming. 156 BOOL themed = [BrowserFrameView drawWindowThemeInDirtyRect:rect 157 forView:self 158 bounds:windowRect 159 offset:NSZeroPoint 160 forceBlackBackground:NO]; 161 162 // If the window needs a title and we painted over the title as drawn by the 163 // default window paint, paint it ourselves. 164 if (themed && gCanDrawTitle && ![[self window] _isTitleHidden]) { 165 [self _drawTitleStringIn:[self _titlebarTitleRect] 166 withColor:[BrowserFrameView titleColorForThemeView:self]]; 167 } 168 169 // Pinstripe the top. 170 if (themed) { 171 NSSize windowPixel = [self convertSizeFromBase:NSMakeSize(1, 1)]; 172 173 windowRect = [self convertRect:[window frame] fromView:nil]; 174 windowRect.origin = NSMakePoint(0, 0); 175 windowRect.origin.y -= 0.5 * windowPixel.height; 176 windowRect.origin.x -= 0.5 * windowPixel.width; 177 windowRect.size.width += windowPixel.width; 178 [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set]; 179 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect 180 xRadius:cornerRadius 181 yRadius:cornerRadius]; 182 [path setLineWidth:windowPixel.width]; 183 [path stroke]; 184 } 185 } 186 187 + (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect 188 forView:(NSView*)view 189 bounds:(NSRect)bounds 190 offset:(NSPoint)offset 191 forceBlackBackground:(BOOL)forceBlackBackground { 192 ui::ThemeProvider* themeProvider = [[view window] themeProvider]; 193 if (!themeProvider) 194 return NO; 195 196 ThemedWindowStyle windowStyle = [[view window] themedWindowStyle]; 197 198 // Devtools windows don't get themed. 199 if (windowStyle & THEMED_DEVTOOLS) 200 return NO; 201 202 BOOL active = [[view window] isMainWindow]; 203 BOOL incognito = windowStyle & THEMED_INCOGNITO; 204 BOOL popup = windowStyle & THEMED_POPUP; 205 206 // Find a theme image. 207 NSColor* themeImageColor = nil; 208 int themeImageID; 209 if (popup && active) 210 themeImageID = IDR_THEME_TOOLBAR; 211 else if (popup && !active) 212 themeImageID = IDR_THEME_TAB_BACKGROUND; 213 else if (!popup && active && incognito) 214 themeImageID = IDR_THEME_FRAME_INCOGNITO; 215 else if (!popup && active && !incognito) 216 themeImageID = IDR_THEME_FRAME; 217 else if (!popup && !active && incognito) 218 themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE; 219 else 220 themeImageID = IDR_THEME_FRAME_INACTIVE; 221 if (themeProvider->HasCustomImage(IDR_THEME_FRAME)) 222 themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID, true); 223 224 // If no theme image, use a gradient if incognito. 225 NSGradient* gradient = nil; 226 if (!themeImageColor && incognito) 227 gradient = themeProvider->GetNSGradient( 228 active ? ThemeService::GRADIENT_FRAME_INCOGNITO : 229 ThemeService::GRADIENT_FRAME_INCOGNITO_INACTIVE); 230 231 BOOL themed = NO; 232 if (themeImageColor) { 233 // The titlebar/tabstrip header on the mac is slightly smaller than on 234 // Windows. To keep the window background lined up with the tab and toolbar 235 // patterns, we have to shift the pattern slightly, rather than simply 236 // drawing it from the top left corner. The offset below was empirically 237 // determined in order to line these patterns up. 238 // 239 // This will make the themes look slightly different than in Windows/Linux 240 // because of the differing heights between window top and tab top, but this 241 // has been approved by UI. 242 NSView* frameView = [[[view window] contentView] superview]; 243 NSPoint topLeft = NSMakePoint(NSMinX(bounds), NSMaxY(bounds)); 244 NSPoint topLeftInFrameCoordinates = 245 [view convertPoint:topLeft toView:frameView]; 246 247 NSPoint phase = kBrowserFrameViewPatternPhaseOffset; 248 phase.x += (offset.x + topLeftInFrameCoordinates.x); 249 phase.y += (offset.y + topLeftInFrameCoordinates.y); 250 251 // Align the phase to physical pixels so resizing the window under HiDPI 252 // doesn't cause wiggling of the theme. 253 phase = [frameView convertPointToBase:phase]; 254 phase.x = floor(phase.x); 255 phase.y = floor(phase.y); 256 phase = [frameView convertPointFromBase:phase]; 257 258 // Default to replacing any existing pixels with the theme image, but if 259 // asked paint black first and blend the theme with black. 260 NSCompositingOperation operation = NSCompositeCopy; 261 if (forceBlackBackground) { 262 [[NSColor blackColor] set]; 263 NSRectFill(dirtyRect); 264 operation = NSCompositeSourceOver; 265 } 266 267 [[NSGraphicsContext currentContext] setPatternPhase:phase]; 268 [themeImageColor set]; 269 NSRectFillUsingOperation(dirtyRect, operation); 270 themed = YES; 271 } else if (gradient) { 272 NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds)); 273 NSPoint endPoint = startPoint; 274 endPoint.y -= kBrowserFrameViewPaintHeight; 275 [gradient drawFromPoint:startPoint toPoint:endPoint options:0]; 276 themed = YES; 277 } 278 279 // Check to see if we have an overlay image. 280 NSImage* overlayImage = nil; 281 if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY)) { 282 overlayImage = themeProvider-> 283 GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY : 284 IDR_THEME_FRAME_OVERLAY_INACTIVE, 285 true); 286 } 287 288 if (overlayImage) { 289 // Anchor to top-left and don't scale. 290 NSSize overlaySize = [overlayImage size]; 291 NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height); 292 [overlayImage drawAtPoint:NSMakePoint(offset.x, 293 NSHeight(bounds) + offset.y - 294 overlaySize.height) 295 fromRect:imageFrame 296 operation:NSCompositeSourceOver 297 fraction:1.0]; 298 } 299 300 return themed; 301 } 302 303 + (NSColor*)titleColorForThemeView:(NSView*)view { 304 ui::ThemeProvider* themeProvider = [[view window] themeProvider]; 305 if (!themeProvider) 306 return [NSColor windowFrameTextColor]; 307 308 ThemedWindowStyle windowStyle = [[view window] themedWindowStyle]; 309 BOOL active = [[view window] isMainWindow]; 310 BOOL incognito = windowStyle & THEMED_INCOGNITO; 311 BOOL popup = windowStyle & THEMED_POPUP; 312 313 NSColor* titleColor = nil; 314 if (popup && active) { 315 titleColor = themeProvider->GetNSColor( 316 ThemeService::COLOR_TAB_TEXT, false); 317 } else if (popup && !active) { 318 titleColor = themeProvider->GetNSColor( 319 ThemeService::COLOR_BACKGROUND_TAB_TEXT, false); 320 } 321 322 if (titleColor) 323 return titleColor; 324 325 if (incognito) 326 return [NSColor whiteColor]; 327 else 328 return [NSColor windowFrameTextColor]; 329 } 330 331 // When the compositor is active, the whole content area is transparent (with 332 // an OpenGL surface behind it), so Cocoa draws the shadow only around the 333 // toolbar area. 334 // Tell the window server that we want a shadow as if none of the content 335 // area is transparent. 336 - (NSUInteger)_shadowFlags { 337 // A slightly less intrusive hack would be to call 338 // _setContentHasShadow:NO on the window. That seems to be what Terminal.app 339 // is doing. However, it leads to this function returning 'code | 64', which 340 // doesn't do what we want. For some reason, it does the right thing in 341 // Terminal.app. 342 // TODO(thakis): Figure out why -_setContentHasShadow: works in Terminal.app 343 // and use that technique instead. http://crbug.com/53382 344 345 // If this isn't the window class we expect, then pass it on to the 346 // original implementation. 347 if (![[self window] isKindOfClass:[FramedBrowserWindow class]]) 348 return [self _shadowFlagsOriginal]; 349 350 return [self _shadowFlagsOriginal] | 128; 351 } 352 353 @end 354