• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
17static const CGFloat kBrowserFrameViewPaintHeight = 60.0;
18static const NSPoint kBrowserFrameViewPatternPhaseOffset = { -5, 3 };
19
20static BOOL gCanDrawTitle = NO;
21static 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