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