Home | History | Annotate | Download | only in cocoa
      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