blob: 881800329d2dd40428c2ba808c9e694a035cdb24 [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 <math.h>
#include <objc/runtime.h>
#include <stddef.h>
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/sdk_forward_declarations.h"
#include "chrome/browser/global_keyboard_shortcuts_mac.h"
#include "chrome/browser/profiles/profile_avatar_icon_util.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/themes/theme_service.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/browser_window_layout.h"
#import "chrome/browser/ui/cocoa/browser_window_touch_bar.h"
#import "chrome/browser/ui/cocoa/browser_window_utils.h"
#include "chrome/browser/ui/cocoa/l10n_util.h"
#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
#import "chrome/browser/ui/cocoa/themed_window.h"
#include "chrome/grit/theme_resources.h"
#include "ui/base/cocoa/cocoa_base_utils.h"
#include "ui/base/cocoa/nsgraphics_context_additions.h"
#import "ui/base/cocoa/nsview_additions.h"
#import "ui/base/cocoa/touch_bar_forward_declarations.h"
// 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
// The NSLayoutConstraint class hierarchy only exists in the 10.11 SDK. When
// targeting something lower, constraintEqualToAnchor:constant: needs to be
// invoked using duck typing.
#if !defined(MAC_OS_X_VERSION_10_11) || \
MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_11
@interface NSObject (NSLayoutConstraint)
- (NSLayoutConstraint*)constraintEqualToAnchor:(id)anchor constant:(CGFloat)c;
- (NSLayoutConstraint*)constraintEqualToAnchor:(id)anchor;
@end
#endif
namespace {
// 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)
// Updates the title bar's frame so it moves the windows buttons to correct
// location (frame bottom is moved down so the buttons are moved down as well).
- (void)adjustTitlebarContainer:(NSView*)titlebarContainer;
// Adds layout constraints to window buttons, respecting flag returned by
// |ShouldFlipWindowControlsInRTL| method.
- (void)setWindowButtonsConstraints;
// Replaces -[NSThemeFrame addTrackingArea:] with implementation that ignores
// tracking rect if its size is the same as the size of window buttons rect
// (rect where close, miniaturize and zoom buttons are located). This is
// needed to workaround macOS bug (rdar://28535344) which unnecessarily adds
// window buttons tracking rect even if those buttons were moved.
// TODO(crbug.com/651287): Remove this workaround once macOS bug is fixed.
- (void)forbidAddingWindowButtonsTrackingArea;
// Called when titlebar container changes its frame. This method adjusts
// titlebar container with correct frame.
- (void)titlebarDidChangeFrameNotification:(NSNotification*)notification;
// Adds layout constraints to the given window button so it displayed at correct
// location. This respects flag returned by |ShouldFlipWindowControlsInRTL|
// method.
- (void)setLeadingOffset:(CGFloat)leadingOffset
toButton:(NSWindowButton)buttonType;
- (void)adjustCloseButton:(NSNotification*)notification;
- (void)adjustMiniaturizeButton:(NSNotification*)notification;
- (void)adjustZoomButton:(NSNotification*)notification;
- (void)adjustButton:(NSButton*)button
ofKind:(NSWindowButton)kind;
- (void)childWindowsDidChange;
@end
@implementation FramedBrowserWindow
+ (CGFloat)browserFrameViewPaintHeight {
return chrome::ShouldUseFullSizeContentView() ? chrome::kTabStripHeight
: 60.0;
}
- (void)setStyleMask:(NSUInteger)styleMask {
if (styleMaskLock_)
return;
[super setStyleMask:styleMask];
}
- (id)initWithContentRect:(NSRect)contentRect
hasTabStrip:(BOOL)hasTabStrip{
NSUInteger styleMask = NSTitledWindowMask |
NSClosableWindowMask |
NSMiniaturizableWindowMask |
NSResizableWindowMask |
NSTexturedBackgroundWindowMask;
bool shouldUseFullSizeContentView =
chrome::ShouldUseFullSizeContentView() && hasTabStrip;
if (shouldUseFullSizeContentView) {
styleMask |= NSFullSizeContentViewWindowMask;
}
if ((self = [super initWithContentRect:contentRect
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:YES
wantsViewsOverTitlebar:hasTabStrip])) {
// 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];
miniaturizeButton_ = [self standardWindowButton:NSWindowMiniaturizeButton];
zoomButton_ = [self standardWindowButton:NSWindowZoomButton];
windowButtonsInterButtonSpacing_ =
NSMinX([miniaturizeButton_ frame]) - NSMaxX([closeButton_ frame]);
if (windowButtonsInterButtonSpacing_ < 0)
// Sierra RTL
windowButtonsInterButtonSpacing_ =
NSMinX([miniaturizeButton_ frame]) - NSMaxX([zoomButton_ frame]);
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
if (shouldUseFullSizeContentView) {
// If Chrome uses full sized content view then window buttons are placed
// inside titlebar (which height is 22 points). In order to move window
// buttons down the whole toolbar should be moved down.
DCHECK(closeButton_);
NSView* titlebarContainer = [[closeButton_ superview] superview];
[self adjustTitlebarContainer:titlebarContainer];
[center addObserver:self
selector:@selector(titlebarDidChangeFrameNotification:)
name:NSViewFrameDidChangeNotification
object:titlebarContainer];
// Window buttons are not movable unless their positioning is forced via
// layout constraints.
[self setWindowButtonsConstraints];
// Remove an extra tracking rect unnecessarily added by AppKit which
// highlights the buttons on mouse enter event. That rect is added where
// buttons used to be previously.
[self forbidAddingWindowButtonsTrackingArea];
} else {
// If Chrome does not use a full sized content view then AppKit adds the
// window buttons to the root view, where they must be manually
// re-positioned.
[self adjustButton:closeButton_ ofKind:NSWindowCloseButton];
[self adjustButton:miniaturizeButton_ ofKind:NSWindowMiniaturizeButton];
[self adjustButton:zoomButton_ ofKind:NSWindowZoomButton];
[closeButton_ setPostsFrameChangedNotifications:YES];
[miniaturizeButton_ setPostsFrameChangedNotifications:YES];
[zoomButton_ setPostsFrameChangedNotifications:YES];
[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_];
}
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
- (void)adjustTitlebarContainer:(NSView*)titlebarContainer {
DCHECK(chrome::ShouldUseFullSizeContentView());
DCHECK([NSStringFromClass([titlebarContainer class])
isEqual:@"NSTitlebarContainerView"]);
NSRect newFrame = [titlebarContainer frame];
NSRect superviewFrame = [[titlebarContainer superview] frame];
// Increase toolbar height to move window buttons down where they should be.
newFrame.size.height =
floor((chrome::kTabStripHeight + NSHeight([closeButton_ frame])) / 2.0);
newFrame.size.width = NSWidth(superviewFrame);
newFrame.origin.y = NSHeight(superviewFrame) - NSHeight(newFrame);
newFrame.origin.x = NSMinX(superviewFrame);
[titlebarContainer setFrame:newFrame];
}
- (void)setWindowButtonsConstraints {
DCHECK(chrome::ShouldUseFullSizeContentView());
CGFloat leadingOffset =
hasTabStrip_ ? kFramedWindowButtonsWithTabStripOffsetFromLeft
: kFramedWindowButtonsWithoutTabStripOffsetFromLeft;
[self setLeadingOffset:leadingOffset toButton:NSWindowCloseButton];
leadingOffset +=
windowButtonsInterButtonSpacing_ + NSWidth([closeButton_ frame]);
[self setLeadingOffset:leadingOffset toButton:NSWindowMiniaturizeButton];
leadingOffset +=
windowButtonsInterButtonSpacing_ + NSWidth([miniaturizeButton_ frame]);
[self setLeadingOffset:leadingOffset toButton:NSWindowZoomButton];
}
- (void)forbidAddingWindowButtonsTrackingArea {
DCHECK(chrome::ShouldUseFullSizeContentView());
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSView* themeFrame = [[[closeButton_ superview] superview] superview];
Class themeFrameClass = [themeFrame class];
DCHECK([NSStringFromClass(themeFrameClass) isEqual:@"NSThemeFrame"]);
SEL addTrackingAreaSelector = @selector(addTrackingArea:);
Method originalMethod =
class_getInstanceMethod(themeFrameClass, addTrackingAreaSelector);
IMP originalImp = method_getImplementation(originalMethod);
NSRect windowButtonsRect = NSUnionRect(
NSUnionRect([closeButton_ frame], [miniaturizeButton_ frame]),
[zoomButton_ frame]);
NSSize buttonsAreaSize = NSIntegralRect(windowButtonsRect).size;
// |newImp| is never released with |imp_removeBlock|.
IMP newImp = imp_implementationWithBlock(^(id self, id area) {
// There is no other way to ensure that |area| is responsible for buttons
// highlighting except by relying on its size.
if (!NSEqualSizes(buttonsAreaSize, NSIntegralRect([area rect]).size)) {
originalImp(self, addTrackingAreaSelector, area);
}
});
// Do not use base::mac::ScopedObjCClassSwizzler as it replaces existing
// implementation which is defined in NSView and will affect the whole app
// performance.
class_replaceMethod(themeFrameClass, addTrackingAreaSelector, newImp,
method_getTypeEncoding(originalMethod));
});
}
- (void)titlebarDidChangeFrameNotification:(NSNotification*)notification {
[self adjustTitlebarContainer:[notification object]];
}
- (void)setLeadingOffset:(CGFloat)leadingOffset
toButton:(NSWindowButton)buttonType {
DCHECK(chrome::ShouldUseFullSizeContentView());
NSButton* button = [self standardWindowButton:buttonType];
[button setTranslatesAutoresizingMaskIntoConstraints:NO];
// Do not use leadingAnchor because |ShouldFlipWindowControlsInRTL|
// should determine if current locale is RTL.
NSLayoutXAxisAnchor* leadingSourceAnchor = [button leftAnchor];
NSLayoutXAxisAnchor* leadingTargetAnchor = [[button superview] leftAnchor];
if (cocoa_l10n_util::ShouldFlipWindowControlsInRTL()) {
leadingSourceAnchor = [button rightAnchor];
leadingTargetAnchor = [[button superview] rightAnchor];
leadingOffset = -leadingOffset;
}
#if !defined(MAC_OS_X_VERSION_10_11) || \
MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_11
id leadingSourceAnchorDuck = leadingSourceAnchor;
#else
NSLayoutXAxisAnchor* leadingSourceAnchorDuck = leadingSourceAnchor;
#endif
[[leadingSourceAnchorDuck constraintEqualToAnchor:leadingTargetAnchor
constant:leadingOffset]
setActive:YES];
[[[button bottomAnchor]
constraintEqualToAnchor:[[button superview] bottomAnchor]]
setActive:YES];
}
- (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];
CGFloat xOffset = hasTabStrip_
? kFramedWindowButtonsWithTabStripOffsetFromLeft
: kFramedWindowButtonsWithoutTabStripOffsetFromLeft;
CGFloat yOffset = hasTabStrip_
? kFramedWindowButtonsWithTabStripOffsetFromTop
: kFramedWindowButtonsWithoutTabStripOffsetFromTop;
buttonFrame.origin =
NSMakePoint(xOffset, (NSHeight([self frame]) -
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;
}
if (cocoa_l10n_util::ShouldFlipWindowControlsInRTL()) {
buttonFrame.origin.x =
NSWidth([self frame]) - buttonFrame.origin.x - NSWidth([button frame]);
}
BOOL didPost = [button postsBoundsChangedNotifications];
[button setPostsFrameChangedNotifications:NO];
[button setFrame:buttonFrame];
[button setPostsFrameChangedNotifications:didPost];
}
// 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 = ui::ConvertPointFromScreenToWindow(self, 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)setShouldHideTitle:(BOOL)flag {
shouldHideTitle_ = flag;
}
- (void)setStyleMaskLock:(BOOL)lock {
styleMaskLock_ = lock;
}
- (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];
}
- (NSPoint)fullScreenButtonOriginAdjustment {
if (!hasTabStrip_)
return NSZeroPoint;
// Vertically center the button.
NSPoint origin = NSMakePoint(0, -6);
// If there is a profile avatar icon present, shift the button over by its
// width and some padding. The new avatar button is displayed to the right
// of the fullscreen icon, so it doesn't need to be shifted.
BrowserWindowController* bwc =
base::mac::ObjCCastStrict<BrowserWindowController>(
[self windowController]);
if ([bwc shouldShowAvatar] && ![bwc shouldUseNewAvatarButton]) {
NSView* avatarButton = [[bwc avatarButtonController] view];
origin.x = -(NSWidth([avatarButton frame]) + 3);
} else {
origin.x -= 6;
}
return origin;
}
+ (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
forView:(NSView*)view
bounds:(NSRect)bounds
forceBlackBackground:(BOOL)forceBlackBackground {
const 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);
}
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];
[[NSGraphicsContext currentContext] cr_setPatternPhase:position
forView:view];
[themeImageColor set];
NSRectFillUsingOperation(dirtyRect, operation);
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.
NSPoint position = [[view window] themeImagePositionForAlignment:
THEME_IMAGE_ALIGN_WITH_FRAME];
position = [view convertPoint:position fromView:nil];
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;
}
- (NSTouchBar*)makeTouchBar {
BrowserWindowController* bwc =
base::mac::ObjCCastStrict<BrowserWindowController>(
[self windowController]);
return [[bwc browserWindowTouchBar] makeTouchBar];
}
- (NSColor*)titleColor {
const 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];
}
- (void)addChildWindow:(NSWindow*)childWindow
ordered:(NSWindowOrderingMode)orderingMode {
[super addChildWindow:childWindow ordered:orderingMode];
[self childWindowsDidChange];
}
- (void)removeChildWindow:(NSWindow*)childWindow {
[super removeChildWindow:childWindow];
[self childWindowsDidChange];
}
- (void)childWindowsDidChange {
id delegate = [self delegate];
if ([delegate respondsToSelector:@selector(childWindowsDidChange)])
[delegate childWindowsDidChange];
}
@end