// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/ui/cocoa/browser_frame_view.h" #import <objc/runtime.h> #import <Carbon/Carbon.h> #include "base/logging.h" #include "base/mac/scoped_nsautorelease_pool.h" #import "chrome/browser/themes/theme_service.h" #import "chrome/browser/ui/cocoa/framed_browser_window.h" #import "chrome/browser/ui/cocoa/themed_window.h" #include "grit/theme_resources.h" static const CGFloat kBrowserFrameViewPaintHeight = 60.0; static const NSPoint kBrowserFrameViewPatternPhaseOffset = { -5, 3 }; static BOOL gCanDrawTitle = NO; static BOOL gCanGetCornerRadius = NO; @interface NSView (Swizzles) - (void)drawRectOriginal:(NSRect)rect; - (NSUInteger)_shadowFlagsOriginal; @end // Undocumented APIs. They are really on NSGrayFrame rather than // BrowserFrameView, but we call them from methods swizzled onto NSGrayFrame. @interface BrowserFrameView (UndocumentedAPI) - (float)roundedCornerRadius; - (CGRect)_titlebarTitleRect; - (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color; - (NSUInteger)_shadowFlags; @end @implementation BrowserFrameView + (void)load { // This is where we swizzle drawRect, and add in two methods that we // need. If any of these fail it shouldn't affect the functionality of the // others. If they all fail, we will lose window frame theming and // roll overs for our close widgets, but things should still function // correctly. base::mac::ScopedNSAutoreleasePool pool; Class grayFrameClass = NSClassFromString(@"NSGrayFrame"); DCHECK(grayFrameClass); if (!grayFrameClass) return; // Exchange draw rect. Method m0 = class_getInstanceMethod([self class], @selector(drawRect:)); DCHECK(m0); if (m0) { BOOL didAdd = class_addMethod(grayFrameClass, @selector(drawRectOriginal:), method_getImplementation(m0), method_getTypeEncoding(m0)); DCHECK(didAdd); if (didAdd) { Method m1 = class_getInstanceMethod(grayFrameClass, @selector(drawRect:)); Method m2 = class_getInstanceMethod(grayFrameClass, @selector(drawRectOriginal:)); DCHECK(m1 && m2); if (m1 && m2) { method_exchangeImplementations(m1, m2); } } } gCanDrawTitle = [grayFrameClass instancesRespondToSelector:@selector(_titlebarTitleRect)] && [grayFrameClass instancesRespondToSelector:@selector(_drawTitleStringIn:withColor:)]; gCanGetCornerRadius = [grayFrameClass instancesRespondToSelector:@selector(roundedCornerRadius)]; // Add _shadowFlags. This is a method on NSThemeFrame, not on NSGrayFrame. // NSThemeFrame is NSGrayFrame's superclass. Class themeFrameClass = NSClassFromString(@"NSThemeFrame"); DCHECK(themeFrameClass); if (!themeFrameClass) return; m0 = class_getInstanceMethod([self class], @selector(_shadowFlags)); DCHECK(m0); if (m0) { BOOL didAdd = class_addMethod(themeFrameClass, @selector(_shadowFlagsOriginal), method_getImplementation(m0), method_getTypeEncoding(m0)); DCHECK(didAdd); if (didAdd) { Method m1 = class_getInstanceMethod(themeFrameClass, @selector(_shadowFlags)); Method m2 = class_getInstanceMethod(themeFrameClass, @selector(_shadowFlagsOriginal)); DCHECK(m1 && m2); if (m1 && m2) { method_exchangeImplementations(m1, m2); } } } } - (id)initWithFrame:(NSRect)frame { // This class is not for instantiating. [self doesNotRecognizeSelector:_cmd]; return nil; } - (id)initWithCoder:(NSCoder*)coder { // This class is not for instantiating. [self doesNotRecognizeSelector:_cmd]; return nil; } // Here is our custom drawing for our frame. - (void)drawRect:(NSRect)rect { // If this isn't the window class we expect, then pass it on to the // original implementation. if (![[self window] isKindOfClass:[FramedBrowserWindow class]]) { [self drawRectOriginal:rect]; return; } // WARNING: There is an obvious optimization opportunity here that you DO NOT // want to take. To save painting cycles, you might think it would be a good // idea to call out to -drawRectOriginal: only if no theme were drawn. In // reality, however, if you fail to call -drawRectOriginal:, or if you call it // after a clipping path is set, the rounded corners at the top of the window // will not draw properly. Do not try to be smart here. // Only paint the top of the window. NSWindow* window = [self window]; NSRect windowRect = [self convertRect:[window frame] fromView:nil]; windowRect.origin = NSMakePoint(0, 0); NSRect paintRect = windowRect; paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight; paintRect.size.height = kBrowserFrameViewPaintHeight; rect = NSIntersectionRect(paintRect, rect); [self drawRectOriginal:rect]; // Set up our clip. float cornerRadius = 4.0; if (gCanGetCornerRadius) cornerRadius = [self roundedCornerRadius]; [[NSBezierPath bezierPathWithRoundedRect:windowRect xRadius:cornerRadius yRadius:cornerRadius] addClip]; [[NSBezierPath bezierPathWithRect:rect] addClip]; // Do the theming. BOOL themed = [BrowserFrameView drawWindowThemeInDirtyRect:rect forView:self bounds:windowRect offset:NSZeroPoint forceBlackBackground:NO]; // If the window needs a title and we painted over the title as drawn by the // default window paint, paint it ourselves. if (themed && gCanDrawTitle && ![[self window] _isTitleHidden]) { [self _drawTitleStringIn:[self _titlebarTitleRect] withColor:[BrowserFrameView titleColorForThemeView:self]]; } // Pinstripe the top. if (themed) { NSSize windowPixel = [self convertSizeFromBase:NSMakeSize(1, 1)]; windowRect = [self convertRect:[window frame] fromView:nil]; windowRect.origin = NSMakePoint(0, 0); windowRect.origin.y -= 0.5 * windowPixel.height; windowRect.origin.x -= 0.5 * windowPixel.width; windowRect.size.width += windowPixel.width; [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set]; NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect xRadius:cornerRadius yRadius:cornerRadius]; [path setLineWidth:windowPixel.width]; [path stroke]; } } + (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect forView:(NSView*)view bounds:(NSRect)bounds offset:(NSPoint)offset forceBlackBackground:(BOOL)forceBlackBackground { ui::ThemeProvider* themeProvider = [[view window] themeProvider]; if (!themeProvider) return NO; ThemedWindowStyle windowStyle = [[view window] themedWindowStyle]; // Devtools windows don't get themed. if (windowStyle & THEMED_DEVTOOLS) return NO; BOOL active = [[view window] isMainWindow]; BOOL incognito = windowStyle & THEMED_INCOGNITO; BOOL popup = windowStyle & THEMED_POPUP; // Find a theme image. NSColor* themeImageColor = nil; int themeImageID; if (popup && active) themeImageID = IDR_THEME_TOOLBAR; else if (popup && !active) themeImageID = IDR_THEME_TAB_BACKGROUND; else if (!popup && active && incognito) themeImageID = IDR_THEME_FRAME_INCOGNITO; else if (!popup && active && !incognito) themeImageID = IDR_THEME_FRAME; else if (!popup && !active && incognito) themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE; else themeImageID = IDR_THEME_FRAME_INACTIVE; if (themeProvider->HasCustomImage(IDR_THEME_FRAME)) themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID, true); // If no theme image, use a gradient if incognito. NSGradient* gradient = nil; if (!themeImageColor && incognito) gradient = themeProvider->GetNSGradient( active ? ThemeService::GRADIENT_FRAME_INCOGNITO : ThemeService::GRADIENT_FRAME_INCOGNITO_INACTIVE); BOOL themed = NO; if (themeImageColor) { // The titlebar/tabstrip header on the mac is slightly smaller than on // Windows. To keep the window background lined up with the tab and toolbar // patterns, we have to shift the pattern slightly, rather than simply // drawing it from the top left corner. The offset below was empirically // determined in order to line these patterns up. // // This will make the themes look slightly different than in Windows/Linux // because of the differing heights between window top and tab top, but this // has been approved by UI. NSView* frameView = [[[view window] contentView] superview]; NSPoint topLeft = NSMakePoint(NSMinX(bounds), NSMaxY(bounds)); NSPoint topLeftInFrameCoordinates = [view convertPoint:topLeft toView:frameView]; NSPoint phase = kBrowserFrameViewPatternPhaseOffset; phase.x += (offset.x + topLeftInFrameCoordinates.x); phase.y += (offset.y + topLeftInFrameCoordinates.y); // Align the phase to physical pixels so resizing the window under HiDPI // doesn't cause wiggling of the theme. phase = [frameView convertPointToBase:phase]; phase.x = floor(phase.x); phase.y = floor(phase.y); phase = [frameView convertPointFromBase:phase]; // Default to replacing any existing pixels with the theme image, but if // asked paint black first and blend the theme with black. NSCompositingOperation operation = NSCompositeCopy; if (forceBlackBackground) { [[NSColor blackColor] set]; NSRectFill(dirtyRect); operation = NSCompositeSourceOver; } [[NSGraphicsContext currentContext] setPatternPhase:phase]; [themeImageColor set]; NSRectFillUsingOperation(dirtyRect, operation); themed = YES; } else if (gradient) { NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds)); NSPoint endPoint = startPoint; endPoint.y -= kBrowserFrameViewPaintHeight; [gradient drawFromPoint:startPoint toPoint:endPoint options:0]; themed = YES; } // Check to see if we have an overlay image. NSImage* overlayImage = nil; if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY)) { overlayImage = themeProvider-> GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY : IDR_THEME_FRAME_OVERLAY_INACTIVE, true); } if (overlayImage) { // Anchor to top-left and don't scale. NSSize overlaySize = [overlayImage size]; NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height); [overlayImage drawAtPoint:NSMakePoint(offset.x, NSHeight(bounds) + offset.y - overlaySize.height) fromRect:imageFrame operation:NSCompositeSourceOver fraction:1.0]; } return themed; } + (NSColor*)titleColorForThemeView:(NSView*)view { ui::ThemeProvider* themeProvider = [[view window] themeProvider]; if (!themeProvider) return [NSColor windowFrameTextColor]; ThemedWindowStyle windowStyle = [[view window] themedWindowStyle]; BOOL active = [[view window] isMainWindow]; BOOL incognito = windowStyle & THEMED_INCOGNITO; BOOL popup = windowStyle & THEMED_POPUP; NSColor* titleColor = nil; if (popup && active) { titleColor = themeProvider->GetNSColor( ThemeService::COLOR_TAB_TEXT, false); } else if (popup && !active) { titleColor = themeProvider->GetNSColor( ThemeService::COLOR_BACKGROUND_TAB_TEXT, false); } if (titleColor) return titleColor; if (incognito) return [NSColor whiteColor]; else return [NSColor windowFrameTextColor]; } // When the compositor is active, the whole content area is transparent (with // an OpenGL surface behind it), so Cocoa draws the shadow only around the // toolbar area. // Tell the window server that we want a shadow as if none of the content // area is transparent. - (NSUInteger)_shadowFlags { // A slightly less intrusive hack would be to call // _setContentHasShadow:NO on the window. That seems to be what Terminal.app // is doing. However, it leads to this function returning 'code | 64', which // doesn't do what we want. For some reason, it does the right thing in // Terminal.app. // TODO(thakis): Figure out why -_setContentHasShadow: works in Terminal.app // and use that technique instead. http://crbug.com/53382 // If this isn't the window class we expect, then pass it on to the // original implementation. if (![[self window] isKindOfClass:[FramedBrowserWindow class]]) return [self _shadowFlagsOriginal]; return [self _shadowFlagsOriginal] | 128; } @end