framed_browser_window.mm revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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 "base/mac/sdk_forward_declarations.h"
9#include "chrome/browser/global_keyboard_shortcuts_mac.h"
10#include "chrome/browser/profiles/profile_avatar_icon_util.h"
11#include "chrome/browser/themes/theme_properties.h"
12#include "chrome/browser/themes/theme_service.h"
13#import "chrome/browser/ui/cocoa/browser_window_controller.h"
14#import "chrome/browser/ui/cocoa/browser_window_utils.h"
15#import "chrome/browser/ui/cocoa/custom_frame_view.h"
16#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
17#import "chrome/browser/ui/cocoa/themed_window.h"
18#include "grit/theme_resources.h"
19#include "ui/base/cocoa/nsgraphics_context_additions.h"
20#import "ui/base/cocoa/nsview_additions.h"
21
22// Implementer's note: Moving the window controls is tricky. When altering the
23// code, ensure that:
24// - accessibility hit testing works
25// - the accessibility hierarchy is correct
26// - close/min in the background don't bring the window forward
27// - rollover effects work correctly
28
29namespace {
30
31const CGFloat kBrowserFrameViewPaintHeight = 60.0;
32
33// Size of the gradient. Empirically determined so that the gradient looks
34// like what the heuristic does when there are just a few tabs.
35const CGFloat kWindowGradientHeight = 24.0;
36
37}
38
39@interface FramedBrowserWindow (Private)
40
41- (void)adjustCloseButton:(NSNotification*)notification;
42- (void)adjustMiniaturizeButton:(NSNotification*)notification;
43- (void)adjustZoomButton:(NSNotification*)notification;
44- (void)adjustButton:(NSButton*)button
45              ofKind:(NSWindowButton)kind;
46- (NSView*)frameView;
47
48@end
49
50// Undocumented APIs. They are really on NSGrayFrame rather than NSView. Take
51// care to only call them on the NSView passed into
52// -[NSWindow drawCustomRect:forView:].
53@interface NSView (UndocumentedAPI)
54
55- (float)roundedCornerRadius;
56- (CGRect)_titlebarTitleRect;
57- (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color;
58
59@end
60
61
62@implementation FramedBrowserWindow
63
64- (id)initWithContentRect:(NSRect)contentRect
65              hasTabStrip:(BOOL)hasTabStrip{
66  NSUInteger styleMask = NSTitledWindowMask |
67                         NSClosableWindowMask |
68                         NSMiniaturizableWindowMask |
69                         NSResizableWindowMask |
70                         NSTexturedBackgroundWindowMask;
71  if ((self = [super initWithContentRect:contentRect
72                               styleMask:styleMask
73                                 backing:NSBackingStoreBuffered
74                                   defer:YES])) {
75    // The 10.6 fullscreen code copies the title to a different window, which
76    // will assert if it's nil.
77    [self setTitle:@""];
78
79    // The following two calls fix http://crbug.com/25684 by preventing the
80    // window from recalculating the border thickness as the window is
81    // resized.
82    // This was causing the window tint to change for the default system theme
83    // when the window was being resized.
84    [self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
85    [self setContentBorderThickness:kWindowGradientHeight forEdge:NSMaxYEdge];
86
87    hasTabStrip_ = hasTabStrip;
88    closeButton_ = [self standardWindowButton:NSWindowCloseButton];
89    [closeButton_ setPostsFrameChangedNotifications:YES];
90    miniaturizeButton_ = [self standardWindowButton:NSWindowMiniaturizeButton];
91    [miniaturizeButton_ setPostsFrameChangedNotifications:YES];
92    zoomButton_ = [self standardWindowButton:NSWindowZoomButton];
93    [zoomButton_ setPostsFrameChangedNotifications:YES];
94
95    windowButtonsInterButtonSpacing_ =
96        NSMinX([miniaturizeButton_ frame]) - NSMaxX([closeButton_ frame]);
97
98    [self adjustButton:closeButton_ ofKind:NSWindowCloseButton];
99    [self adjustButton:miniaturizeButton_ ofKind:NSWindowMiniaturizeButton];
100    [self adjustButton:zoomButton_ ofKind:NSWindowZoomButton];
101
102    NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
103    [center addObserver:self
104               selector:@selector(adjustCloseButton:)
105                   name:NSViewFrameDidChangeNotification
106                 object:closeButton_];
107    [center addObserver:self
108               selector:@selector(adjustMiniaturizeButton:)
109                   name:NSViewFrameDidChangeNotification
110                 object:miniaturizeButton_];
111    [center addObserver:self
112               selector:@selector(adjustZoomButton:)
113                   name:NSViewFrameDidChangeNotification
114                 object:zoomButton_];
115    [center addObserver:self
116               selector:@selector(themeDidChangeNotification:)
117                   name:kBrowserThemeDidChangeNotification
118                 object:nil];
119  }
120
121  return self;
122}
123
124- (void)dealloc {
125  [[NSNotificationCenter defaultCenter] removeObserver:self];
126  [super dealloc];
127}
128
129- (void)adjustCloseButton:(NSNotification*)notification {
130  [self adjustButton:[notification object]
131              ofKind:NSWindowCloseButton];
132}
133
134- (void)adjustMiniaturizeButton:(NSNotification*)notification {
135  [self adjustButton:[notification object]
136              ofKind:NSWindowMiniaturizeButton];
137}
138
139- (void)adjustZoomButton:(NSNotification*)notification {
140  [self adjustButton:[notification object]
141              ofKind:NSWindowZoomButton];
142}
143
144- (void)adjustButton:(NSButton*)button
145              ofKind:(NSWindowButton)kind {
146  NSRect buttonFrame = [button frame];
147  NSRect frameViewBounds = [[self frameView] bounds];
148
149  CGFloat xOffset = hasTabStrip_
150      ? kFramedWindowButtonsWithTabStripOffsetFromLeft
151      : kFramedWindowButtonsWithoutTabStripOffsetFromLeft;
152  CGFloat yOffset = hasTabStrip_
153      ? kFramedWindowButtonsWithTabStripOffsetFromTop
154      : kFramedWindowButtonsWithoutTabStripOffsetFromTop;
155  buttonFrame.origin =
156      NSMakePoint(xOffset, (NSHeight(frameViewBounds) -
157                            NSHeight(buttonFrame) - yOffset));
158
159  switch (kind) {
160    case NSWindowZoomButton:
161      buttonFrame.origin.x += NSWidth([miniaturizeButton_ frame]);
162      buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
163      // fallthrough
164    case NSWindowMiniaturizeButton:
165      buttonFrame.origin.x += NSWidth([closeButton_ frame]);
166      buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
167      // fallthrough
168    default:
169      break;
170  }
171
172  BOOL didPost = [button postsBoundsChangedNotifications];
173  [button setPostsFrameChangedNotifications:NO];
174  [button setFrame:buttonFrame];
175  [button setPostsFrameChangedNotifications:didPost];
176}
177
178- (NSView*)frameView {
179  return [[self contentView] superview];
180}
181
182// The tab strip view covers our window buttons. So we add hit testing here
183// to find them properly and return them to the accessibility system.
184- (id)accessibilityHitTest:(NSPoint)point {
185  NSPoint windowPoint = [self convertScreenToBase:point];
186  NSControl* controls[] = { closeButton_, zoomButton_, miniaturizeButton_ };
187  id value = nil;
188  for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); ++i) {
189    if (NSPointInRect(windowPoint, [controls[i] frame])) {
190      value = [controls[i] accessibilityHitTest:point];
191      break;
192    }
193  }
194  if (!value) {
195    value = [super accessibilityHitTest:point];
196  }
197  return value;
198}
199
200- (void)windowMainStatusChanged {
201  NSView* frameView = [self frameView];
202  NSView* contentView = [self contentView];
203  NSRect updateRect = [frameView frame];
204  NSRect contentRect = [contentView frame];
205  CGFloat tabStripHeight = [TabStripController defaultTabHeight];
206  updateRect.size.height -= NSHeight(contentRect) - tabStripHeight;
207  updateRect.origin.y = NSMaxY(contentRect) - tabStripHeight;
208  [[self frameView] setNeedsDisplayInRect:updateRect];
209}
210
211- (void)becomeMainWindow {
212  [self windowMainStatusChanged];
213  [super becomeMainWindow];
214}
215
216- (void)resignMainWindow {
217  [self windowMainStatusChanged];
218  [super resignMainWindow];
219}
220
221// Called after the current theme has changed.
222- (void)themeDidChangeNotification:(NSNotification*)aNotification {
223  [[self frameView] setNeedsDisplay:YES];
224}
225
226- (void)sendEvent:(NSEvent*)event {
227  // For Cocoa windows, clicking on the close and the miniaturize buttons (but
228  // not the zoom button) while a window is in the background does NOT bring
229  // that window to the front. We don't get that behavior for free (probably
230  // because the tab strip view covers those buttons), so we handle it here.
231  // Zoom buttons do bring the window to the front. Note that Finder windows (in
232  // Leopard) behave differently in this regard in that zoom buttons don't bring
233  // the window to the foreground.
234  BOOL eventHandled = NO;
235  if (![self isMainWindow]) {
236    if ([event type] == NSLeftMouseDown) {
237      NSView* frameView = [self frameView];
238      NSPoint mouse = [frameView convertPoint:[event locationInWindow]
239                                     fromView:nil];
240      if (NSPointInRect(mouse, [closeButton_ frame])) {
241        [closeButton_ mouseDown:event];
242        eventHandled = YES;
243      } else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) {
244        [miniaturizeButton_ mouseDown:event];
245        eventHandled = YES;
246      }
247    }
248  }
249  if (!eventHandled) {
250    [super sendEvent:event];
251  }
252}
253
254- (void)setShouldHideTitle:(BOOL)flag {
255  shouldHideTitle_ = flag;
256}
257
258- (BOOL)_isTitleHidden {
259  return shouldHideTitle_;
260}
261
262- (CGFloat)windowButtonsInterButtonSpacing {
263  return windowButtonsInterButtonSpacing_;
264}
265
266// This method is called whenever a window is moved in order to ensure it fits
267// on the screen.  We cannot always handle resizes without breaking, so we
268// prevent frame constraining in those cases.
269- (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen {
270  // Do not constrain the frame rect if our delegate says no.  In this case,
271  // return the original (unconstrained) frame.
272  id delegate = [self delegate];
273  if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] &&
274      ![delegate shouldConstrainFrameRect])
275    return frame;
276
277  return [super constrainFrameRect:frame toScreen:screen];
278}
279
280// This method is overridden in order to send the toggle fullscreen message
281// through the cross-platform browser framework before going fullscreen.  The
282// message will eventually come back as a call to |-toggleSystemFullScreen|,
283// which in turn calls AppKit's |NSWindow -toggleFullScreen:|.
284- (void)toggleFullScreen:(id)sender {
285  id delegate = [self delegate];
286  if ([delegate respondsToSelector:@selector(handleLionToggleFullscreen)])
287    [delegate handleLionToggleFullscreen];
288}
289
290- (void)toggleSystemFullScreen {
291  if ([super respondsToSelector:@selector(toggleFullScreen:)])
292    [super toggleFullScreen:nil];
293}
294
295- (NSPoint)fullScreenButtonOriginAdjustment {
296  if (!hasTabStrip_)
297    return NSZeroPoint;
298
299  // Vertically center the button.
300  NSPoint origin = NSMakePoint(0, -6);
301
302  // If there is a profile avatar icon present, shift the button over by its
303  // width and some padding. The new avatar button is displayed to the right
304  // of the fullscreen icon, so it doesn't need to be shifted.
305  BrowserWindowController* bwc =
306      static_cast<BrowserWindowController*>([self windowController]);
307  if ([bwc shouldShowAvatar] && ![bwc shouldUseNewAvatarButton]) {
308    NSView* avatarButton = [[bwc avatarButtonController] view];
309    origin.x = -(NSWidth([avatarButton frame]) + 3);
310  } else {
311    origin.x -= 6;
312  }
313
314  return origin;
315}
316
317- (void)drawCustomFrameRect:(NSRect)rect forView:(NSView*)view {
318  // WARNING: There is an obvious optimization opportunity here that you DO NOT
319  // want to take. To save painting cycles, you might think it would be a good
320  // idea to call out to the default implementation only if no theme were
321  // drawn. In reality, however, if you fail to call the default
322  // implementation, or if you call it after a clipping path is set, the
323  // rounded corners at the top of the window will not draw properly. Do not
324  // try to be smart here.
325
326  // Only paint the top of the window.
327  NSRect windowRect = [view convertRect:[self frame] fromView:nil];
328  windowRect.origin = NSZeroPoint;
329
330  NSRect paintRect = windowRect;
331  paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight;
332  paintRect.size.height = kBrowserFrameViewPaintHeight;
333  rect = NSIntersectionRect(paintRect, rect);
334  [super drawCustomFrameRect:rect forView:view];
335
336  // Set up our clip.
337  float cornerRadius = 4.0;
338  if ([view respondsToSelector:@selector(roundedCornerRadius)])
339    cornerRadius = [view roundedCornerRadius];
340  [[NSBezierPath bezierPathWithRoundedRect:windowRect
341                                   xRadius:cornerRadius
342                                   yRadius:cornerRadius] addClip];
343  [[NSBezierPath bezierPathWithRect:rect] addClip];
344
345  // Do the theming.
346  BOOL themed = [FramedBrowserWindow
347      drawWindowThemeInDirtyRect:rect
348                         forView:view
349                          bounds:windowRect
350            forceBlackBackground:NO];
351
352  // If the window needs a title and we painted over the title as drawn by the
353  // default window paint, paint it ourselves.
354  if (themed && [view respondsToSelector:@selector(_titlebarTitleRect)] &&
355      [view respondsToSelector:@selector(_drawTitleStringIn:withColor:)] &&
356      ![self _isTitleHidden]) {
357    [view _drawTitleStringIn:[view _titlebarTitleRect]
358                   withColor:[self titleColor]];
359  }
360
361  // Pinstripe the top.
362  if (themed) {
363    CGFloat lineWidth = [view cr_lineWidth];
364
365    windowRect = [view convertRect:[self frame] fromView:nil];
366    windowRect.origin = NSZeroPoint;
367    windowRect.origin.y -= 0.5 * lineWidth;
368    windowRect.origin.x -= 0.5 * lineWidth;
369    windowRect.size.width += lineWidth;
370    [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set];
371    NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect
372                                                         xRadius:cornerRadius
373                                                         yRadius:cornerRadius];
374    [path setLineWidth:lineWidth];
375    [path stroke];
376  }
377}
378
379+ (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
380                           forView:(NSView*)view
381                            bounds:(NSRect)bounds
382              forceBlackBackground:(BOOL)forceBlackBackground {
383  ui::ThemeProvider* themeProvider = [[view window] themeProvider];
384  if (!themeProvider)
385    return NO;
386
387  ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
388
389  // Devtools windows don't get themed.
390  if (windowStyle & THEMED_DEVTOOLS)
391    return NO;
392
393  BOOL active = [[view window] isMainWindow];
394  BOOL incognito = windowStyle & THEMED_INCOGNITO;
395  BOOL popup = windowStyle & THEMED_POPUP;
396
397  // Find a theme image.
398  NSColor* themeImageColor = nil;
399  if (!popup) {
400    int themeImageID;
401    if (active && incognito)
402      themeImageID = IDR_THEME_FRAME_INCOGNITO;
403    else if (active && !incognito)
404      themeImageID = IDR_THEME_FRAME;
405    else if (!active && incognito)
406      themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE;
407    else
408      themeImageID = IDR_THEME_FRAME_INACTIVE;
409    if (themeProvider->HasCustomImage(IDR_THEME_FRAME))
410      themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID);
411  }
412
413  // If no theme image, use a gradient if incognito.
414  NSGradient* gradient = nil;
415  if (!themeImageColor && incognito)
416    gradient = themeProvider->GetNSGradient(
417        active ? ThemeProperties::GRADIENT_FRAME_INCOGNITO :
418                 ThemeProperties::GRADIENT_FRAME_INCOGNITO_INACTIVE);
419
420  BOOL themed = NO;
421  if (themeImageColor) {
422    // Default to replacing any existing pixels with the theme image, but if
423    // asked paint black first and blend the theme with black.
424    NSCompositingOperation operation = NSCompositeCopy;
425    if (forceBlackBackground) {
426      [[NSColor blackColor] set];
427      NSRectFill(dirtyRect);
428      operation = NSCompositeSourceOver;
429    }
430
431    NSPoint position = [[view window] themeImagePositionForAlignment:
432        THEME_IMAGE_ALIGN_WITH_FRAME];
433
434    // Align the phase to physical pixels so resizing the window under HiDPI
435    // doesn't cause wiggling of the theme.
436    NSView* frameView = [[[view window] contentView] superview];
437    position = [frameView convertPointToBase:position];
438    position.x = floor(position.x);
439    position.y = floor(position.y);
440    position = [frameView convertPointFromBase:position];
441    [[NSGraphicsContext currentContext] cr_setPatternPhase:position
442                                                   forView:view];
443
444    [themeImageColor set];
445    NSRectFillUsingOperation(dirtyRect, operation);
446    themed = YES;
447  } else if (gradient) {
448    NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds));
449    NSPoint endPoint = startPoint;
450    endPoint.y -= kBrowserFrameViewPaintHeight;
451    [gradient drawFromPoint:startPoint toPoint:endPoint options:0];
452    themed = YES;
453  }
454
455  // Check to see if we have an overlay image.
456  NSImage* overlayImage = nil;
457  if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && !incognito &&
458      !popup) {
459    overlayImage = themeProvider->
460        GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY :
461                                 IDR_THEME_FRAME_OVERLAY_INACTIVE);
462  }
463
464  if (overlayImage) {
465    // Anchor to top-left and don't scale.
466    NSView* frameView = [[[view window] contentView] superview];
467    NSPoint position = [[view window] themeImagePositionForAlignment:
468        THEME_IMAGE_ALIGN_WITH_FRAME];
469    position = [view convertPoint:position fromView:frameView];
470    NSSize overlaySize = [overlayImage size];
471    NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height);
472    [overlayImage drawAtPoint:NSMakePoint(position.x,
473                                          position.y - overlaySize.height)
474                     fromRect:imageFrame
475                    operation:NSCompositeSourceOver
476                     fraction:1.0];
477  }
478
479  return themed;
480}
481
482- (NSColor*)titleColor {
483  ui::ThemeProvider* themeProvider = [self themeProvider];
484  if (!themeProvider)
485    return [NSColor windowFrameTextColor];
486
487  ThemedWindowStyle windowStyle = [self themedWindowStyle];
488  BOOL incognito = windowStyle & THEMED_INCOGNITO;
489
490  if (incognito)
491    return [NSColor whiteColor];
492  else
493    return [NSColor windowFrameTextColor];
494}
495
496@end
497