blob: 6c143c507d7a275abefa907277272372f5b49ab6 [file] [log] [blame]
// Copyright (c) 2012 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/framed_browser_window.h"
#include "base/logging.h"
#include "chrome/browser/global_keyboard_shortcuts_mac.h"
#include "chrome/browser/profiles/profile_info_util.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/browser_window_utils.h"
#import "chrome/browser/ui/cocoa/custom_frame_view.h"
#import "chrome/browser/ui/cocoa/nsview_additions.h"
#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
#import "chrome/browser/ui/cocoa/themed_window.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/themes/theme_service.h"
#include "grit/theme_resources.h"
#include "ui/base/cocoa/nsgraphics_context_additions.h"
// Replicate specific 10.7 SDK declarations for building with prior SDKs.
#if !defined(MAC_OS_X_VERSION_10_7) || \
MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
@interface NSWindow (LionSDKDeclarations)
- (void)toggleFullScreen:(id)sender;
@end
enum {
NSWindowDocumentVersionsButton = 6,
NSWindowFullScreenButton
};
#endif // MAC_OS_X_VERSION_10_7
// Implementer's note: Moving the window controls is tricky. When altering the
// code, ensure that:
// - accessibility hit testing works
// - the accessibility hierarchy is correct
// - close/min in the background don't bring the window forward
// - rollover effects work correctly
namespace {
const CGFloat kBrowserFrameViewPaintHeight = 60.0;
// Size of the gradient. Empirically determined so that the gradient looks
// like what the heuristic does when there are just a few tabs.
const CGFloat kWindowGradientHeight = 24.0;
}
@interface FramedBrowserWindow (Private)
- (void)adjustCloseButton:(NSNotification*)notification;
- (void)adjustMiniaturizeButton:(NSNotification*)notification;
- (void)adjustZoomButton:(NSNotification*)notification;
- (void)adjustButton:(NSButton*)button
ofKind:(NSWindowButton)kind;
- (NSView*)frameView;
@end
// Undocumented APIs. They are really on NSGrayFrame rather than NSView. Take
// care to only call them on the NSView passed into
// -[NSWindow drawCustomRect:forView:].
@interface NSView (UndocumentedAPI)
- (float)roundedCornerRadius;
- (CGRect)_titlebarTitleRect;
- (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color;
@end
@implementation FramedBrowserWindow
- (id)initWithContentRect:(NSRect)contentRect
hasTabStrip:(BOOL)hasTabStrip{
NSUInteger styleMask = NSTitledWindowMask |
NSClosableWindowMask |
NSMiniaturizableWindowMask |
NSResizableWindowMask |
NSTexturedBackgroundWindowMask;
if ((self = [super initWithContentRect:contentRect
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:YES])) {
// The 10.6 fullscreen code copies the title to a different window, which
// will assert if it's nil.
[self setTitle:@""];
// The following two calls fix http://crbug.com/25684 by preventing the
// window from recalculating the border thickness as the window is
// resized.
// This was causing the window tint to change for the default system theme
// when the window was being resized.
[self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
[self setContentBorderThickness:kWindowGradientHeight forEdge:NSMaxYEdge];
hasTabStrip_ = hasTabStrip;
closeButton_ = [self standardWindowButton:NSWindowCloseButton];
[closeButton_ setPostsFrameChangedNotifications:YES];
miniaturizeButton_ = [self standardWindowButton:NSWindowMiniaturizeButton];
[miniaturizeButton_ setPostsFrameChangedNotifications:YES];
zoomButton_ = [self standardWindowButton:NSWindowZoomButton];
[zoomButton_ setPostsFrameChangedNotifications:YES];
windowButtonsInterButtonSpacing_ =
NSMinX([miniaturizeButton_ frame]) - NSMaxX([closeButton_ frame]);
[self adjustButton:closeButton_ ofKind:NSWindowCloseButton];
[self adjustButton:miniaturizeButton_ ofKind:NSWindowMiniaturizeButton];
[self adjustButton:zoomButton_ ofKind:NSWindowZoomButton];
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(adjustCloseButton:)
name:NSViewFrameDidChangeNotification
object:closeButton_];
[center addObserver:self
selector:@selector(adjustMiniaturizeButton:)
name:NSViewFrameDidChangeNotification
object:miniaturizeButton_];
[center addObserver:self
selector:@selector(adjustZoomButton:)
name:NSViewFrameDidChangeNotification
object:zoomButton_];
[center addObserver:self
selector:@selector(themeDidChangeNotification:)
name:kBrowserThemeDidChangeNotification
object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
- (void)adjustCloseButton:(NSNotification*)notification {
[self adjustButton:[notification object]
ofKind:NSWindowCloseButton];
}
- (void)adjustMiniaturizeButton:(NSNotification*)notification {
[self adjustButton:[notification object]
ofKind:NSWindowMiniaturizeButton];
}
- (void)adjustZoomButton:(NSNotification*)notification {
[self adjustButton:[notification object]
ofKind:NSWindowZoomButton];
}
- (void)adjustButton:(NSButton*)button
ofKind:(NSWindowButton)kind {
NSRect buttonFrame = [button frame];
NSRect frameViewBounds = [[self frameView] bounds];
CGFloat xOffset = hasTabStrip_
? kFramedWindowButtonsWithTabStripOffsetFromLeft
: kFramedWindowButtonsWithoutTabStripOffsetFromLeft;
CGFloat yOffset = hasTabStrip_
? kFramedWindowButtonsWithTabStripOffsetFromTop
: kFramedWindowButtonsWithoutTabStripOffsetFromTop;
buttonFrame.origin =
NSMakePoint(xOffset, (NSHeight(frameViewBounds) -
NSHeight(buttonFrame) - yOffset));
switch (kind) {
case NSWindowZoomButton:
buttonFrame.origin.x += NSWidth([miniaturizeButton_ frame]);
buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
// fallthrough
case NSWindowMiniaturizeButton:
buttonFrame.origin.x += NSWidth([closeButton_ frame]);
buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
// fallthrough
default:
break;
}
BOOL didPost = [button postsBoundsChangedNotifications];
[button setPostsFrameChangedNotifications:NO];
[button setFrame:buttonFrame];
[button setPostsFrameChangedNotifications:didPost];
}
- (NSView*)frameView {
return [[self contentView] superview];
}
// The tab strip view covers our window buttons. So we add hit testing here
// to find them properly and return them to the accessibility system.
- (id)accessibilityHitTest:(NSPoint)point {
NSPoint windowPoint = [self convertScreenToBase:point];
NSControl* controls[] = { closeButton_, zoomButton_, miniaturizeButton_ };
id value = nil;
for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); ++i) {
if (NSPointInRect(windowPoint, [controls[i] frame])) {
value = [controls[i] accessibilityHitTest:point];
break;
}
}
if (!value) {
value = [super accessibilityHitTest:point];
}
return value;
}
- (void)windowMainStatusChanged {
NSView* frameView = [self frameView];
NSView* contentView = [self contentView];
NSRect updateRect = [frameView frame];
NSRect contentRect = [contentView frame];
CGFloat tabStripHeight = [TabStripController defaultTabHeight];
updateRect.size.height -= NSHeight(contentRect) - tabStripHeight;
updateRect.origin.y = NSMaxY(contentRect) - tabStripHeight;
[[self frameView] setNeedsDisplayInRect:updateRect];
}
- (void)becomeMainWindow {
[self windowMainStatusChanged];
[super becomeMainWindow];
}
- (void)resignMainWindow {
[self windowMainStatusChanged];
[super resignMainWindow];
}
// Called after the current theme has changed.
- (void)themeDidChangeNotification:(NSNotification*)aNotification {
[[self frameView] setNeedsDisplay:YES];
}
- (void)sendEvent:(NSEvent*)event {
// For Cocoa windows, clicking on the close and the miniaturize buttons (but
// not the zoom button) while a window is in the background does NOT bring
// that window to the front. We don't get that behavior for free (probably
// because the tab strip view covers those buttons), so we handle it here.
// Zoom buttons do bring the window to the front. Note that Finder windows (in
// Leopard) behave differently in this regard in that zoom buttons don't bring
// the window to the foreground.
BOOL eventHandled = NO;
if (![self isMainWindow]) {
if ([event type] == NSLeftMouseDown) {
NSView* frameView = [self frameView];
NSPoint mouse = [frameView convertPoint:[event locationInWindow]
fromView:nil];
if (NSPointInRect(mouse, [closeButton_ frame])) {
[closeButton_ mouseDown:event];
eventHandled = YES;
} else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) {
[miniaturizeButton_ mouseDown:event];
eventHandled = YES;
}
}
}
if (!eventHandled) {
[super sendEvent:event];
}
}
- (void)setShouldHideTitle:(BOOL)flag {
shouldHideTitle_ = flag;
}
- (BOOL)_isTitleHidden {
return shouldHideTitle_;
}
- (CGFloat)windowButtonsInterButtonSpacing {
return windowButtonsInterButtonSpacing_;
}
// This method is called whenever a window is moved in order to ensure it fits
// on the screen. We cannot always handle resizes without breaking, so we
// prevent frame constraining in those cases.
- (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen {
// Do not constrain the frame rect if our delegate says no. In this case,
// return the original (unconstrained) frame.
id delegate = [self delegate];
if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] &&
![delegate shouldConstrainFrameRect])
return frame;
return [super constrainFrameRect:frame toScreen:screen];
}
// This method is overridden in order to send the toggle fullscreen message
// through the cross-platform browser framework before going fullscreen. The
// message will eventually come back as a call to |-toggleSystemFullScreen|,
// which in turn calls AppKit's |NSWindow -toggleFullScreen:|.
- (void)toggleFullScreen:(id)sender {
id delegate = [self delegate];
if ([delegate respondsToSelector:@selector(handleLionToggleFullscreen)])
[delegate handleLionToggleFullscreen];
}
- (void)toggleSystemFullScreen {
if ([super respondsToSelector:@selector(toggleFullScreen:)])
[super toggleFullScreen:nil];
}
- (NSPoint)fullScreenButtonOriginAdjustment {
if (!hasTabStrip_)
return NSZeroPoint;
// Vertically center the button.
NSPoint origin = NSMakePoint(0, -6);
// If there is a profile avatar present, shift the button over by its
// width and some padding.
BrowserWindowController* bwc =
static_cast<BrowserWindowController*>([self windowController]);
if ([bwc shouldShowAvatar]) {
AvatarButtonController* avatarButtonVC = [bwc avatarButtonController];
NSView* avatarButton = [avatarButtonVC view];
origin.x = -(NSWidth([avatarButton frame]) + 3);
} else {
origin.x -= 6;
}
return origin;
}
- (void)drawCustomFrameRect:(NSRect)rect forView:(NSView*)view {
// 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 the default implementation only if no theme were
// drawn. In reality, however, if you fail to call the default
// implementation, 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.
NSRect windowRect = [view convertRect:[self frame] fromView:nil];
windowRect.origin = NSZeroPoint;
NSRect paintRect = windowRect;
paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight;
paintRect.size.height = kBrowserFrameViewPaintHeight;
rect = NSIntersectionRect(paintRect, rect);
[super drawCustomFrameRect:rect forView:view];
// Set up our clip.
float cornerRadius = 4.0;
if ([view respondsToSelector:@selector(roundedCornerRadius)])
cornerRadius = [view roundedCornerRadius];
[[NSBezierPath bezierPathWithRoundedRect:windowRect
xRadius:cornerRadius
yRadius:cornerRadius] addClip];
[[NSBezierPath bezierPathWithRect:rect] addClip];
// Do the theming.
BOOL themed = [FramedBrowserWindow
drawWindowThemeInDirtyRect:rect
forView:view
bounds:windowRect
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 && [view respondsToSelector:@selector(_titlebarTitleRect)] &&
[view respondsToSelector:@selector(_drawTitleStringIn:withColor:)] &&
![self _isTitleHidden]) {
[view _drawTitleStringIn:[view _titlebarTitleRect]
withColor:[self titleColor]];
}
// Pinstripe the top.
if (themed) {
CGFloat lineWidth = [view cr_lineWidth];
windowRect = [view convertRect:[self frame] fromView:nil];
windowRect.origin = NSZeroPoint;
windowRect.origin.y -= 0.5 * lineWidth;
windowRect.origin.x -= 0.5 * lineWidth;
windowRect.size.width += lineWidth;
[[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set];
NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect
xRadius:cornerRadius
yRadius:cornerRadius];
[path setLineWidth:lineWidth];
[path stroke];
}
}
+ (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
forView:(NSView*)view
bounds:(NSRect)bounds
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;
if (!popup) {
int themeImageID;
if (active && incognito)
themeImageID = IDR_THEME_FRAME_INCOGNITO;
else if (active && !incognito)
themeImageID = IDR_THEME_FRAME;
else if (!active && incognito)
themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE;
else
themeImageID = IDR_THEME_FRAME_INACTIVE;
if (themeProvider->HasCustomImage(IDR_THEME_FRAME))
themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID);
}
// If no theme image, use a gradient if incognito.
NSGradient* gradient = nil;
if (!themeImageColor && incognito)
gradient = themeProvider->GetNSGradient(
active ? ThemeProperties::GRADIENT_FRAME_INCOGNITO :
ThemeProperties::GRADIENT_FRAME_INCOGNITO_INACTIVE);
BOOL themed = NO;
if (themeImageColor) {
// 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;
}
NSPoint position = [[view window] themeImagePositionForAlignment:
THEME_IMAGE_ALIGN_WITH_FRAME];
// Align the phase to physical pixels so resizing the window under HiDPI
// doesn't cause wiggling of the theme.
NSView* frameView = [[[view window] contentView] superview];
position = [frameView convertPointToBase:position];
position.x = floor(position.x);
position.y = floor(position.y);
position = [frameView convertPointFromBase:position];
[[NSGraphicsContext currentContext] cr_setPatternPhase:position
forView:view];
[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) && !incognito &&
!popup) {
overlayImage = themeProvider->
GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY :
IDR_THEME_FRAME_OVERLAY_INACTIVE);
}
if (overlayImage) {
// Anchor to top-left and don't scale.
NSView* frameView = [[[view window] contentView] superview];
NSPoint position = [[view window] themeImagePositionForAlignment:
THEME_IMAGE_ALIGN_WITH_FRAME];
position = [view convertPoint:position fromView:frameView];
NSSize overlaySize = [overlayImage size];
NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height);
[overlayImage drawAtPoint:NSMakePoint(position.x,
position.y - overlaySize.height)
fromRect:imageFrame
operation:NSCompositeSourceOver
fraction:1.0];
}
return themed;
}
- (NSColor*)titleColor {
ui::ThemeProvider* themeProvider = [self themeProvider];
if (!themeProvider)
return [NSColor windowFrameTextColor];
ThemedWindowStyle windowStyle = [self themedWindowStyle];
BOOL incognito = windowStyle & THEMED_INCOGNITO;
if (incognito)
return [NSColor whiteColor];
else
return [NSColor windowFrameTextColor];
}
@end