blob: 4755fe0c12b4906bbc939135777cc147632b4fb2 [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.
#include "chrome/browser/ui/cocoa/gradient_button_cell.h"
#include <cmath>
#include "base/logging.h"
#import "base/mac/scoped_nsobject.h"
#import "chrome/browser/themes/theme_properties.h"
#import "chrome/browser/themes/theme_service.h"
#import "chrome/browser/ui/cocoa/rect_path_utils.h"
#import "chrome/browser/ui/cocoa/themed_window.h"
#include "chrome/grit/theme_resources.h"
#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
#import "ui/base/cocoa/nsgraphics_context_additions.h"
#import "ui/base/cocoa/nsview_additions.h"
#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
@interface GradientButtonCell (Private)
- (void)sharedInit;
// Get drawing parameters for a given cell frame in a given view. The inner
// frame is the one required by |-drawInteriorWithFrame:inView:|. The inner and
// outer paths are the ones required by |-drawBorderAndFillForTheme:...|. The
// outer path also gives the area in which to clip. Any of the |return...|
// arguments may be NULL (in which case the given parameter won't be returned).
// If |returnInnerPath| or |returnOuterPath|, |*returnInnerPath| or
// |*returnOuterPath| should be nil, respectively.
- (void)getDrawParamsForFrame:(NSRect)cellFrame
inView:(NSView*)controlView
innerFrame:(NSRect*)returnInnerFrame
innerPath:(NSBezierPath**)returnInnerPath
clipPath:(NSBezierPath**)returnClipPath;
- (void)updateTrackingAreas;
@end
static const NSTimeInterval kAnimationShowDuration = 0.2;
// Note: due to a bug (?), drawWithFrame:inView: does not call
// drawBorderAndFillForTheme::::: unless the mouse is inside. The net
// effect is that our "fade out" when the mouse leaves becaumes
// instantaneous. When I "fixed" it things looked horrible; the
// hover-overed bookmark button would stay highlit for 0.4 seconds
// which felt like latency/lag. I'm leaving the "bug" in place for
// now so we don't suck. -jrg
static const NSTimeInterval kAnimationHideDuration = 0.4;
@implementation GradientButtonCell
@synthesize hoverAlpha = hoverAlpha_;
+ (CGFloat)insetInView:(NSView*)view {
return [view cr_lineWidth];
}
// For nib instantiations
- (id)initWithCoder:(NSCoder*)decoder {
if ((self = [super initWithCoder:decoder])) {
[self sharedInit];
}
return self;
}
// For programmatic instantiations
- (id)initTextCell:(NSString*)string {
if ((self = [super initTextCell:string])) {
[self sharedInit];
}
return self;
}
- (void)dealloc {
if (trackingArea_) {
[[self controlView] removeTrackingArea:trackingArea_];
trackingArea_.reset();
}
[super dealloc];
}
// Return YES if we are pulsing (towards another state or continuously).
- (BOOL)pulsing {
if ((pulseState_ == gradient_button_cell::kPulsingOn) ||
(pulseState_ == gradient_button_cell::kPulsingOff))
return YES;
return NO;
}
// Perform one pulse step when animating a pulse.
- (void)performOnePulseStep {
NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
CGFloat opacity = [self hoverAlpha];
// Update opacity based on state.
// Adjust state if we have finished.
switch (pulseState_) {
case gradient_button_cell::kPulsingOn:
opacity += elapsed / kAnimationShowDuration;
if (opacity > 1.0) {
[self setPulseState:gradient_button_cell::kPulsedOn];
return;
}
break;
case gradient_button_cell::kPulsingOff:
opacity -= elapsed / kAnimationHideDuration;
if (opacity < 0.0) {
[self setPulseState:gradient_button_cell::kPulsedOff];
return;
}
break;
case gradient_button_cell::kPulsingStuckOn:
outerStrokeAlphaMult_ = 1.0;
break;
default:
NOTREACHED() << "unknown pulse state";
}
// Update our control.
lastHoverUpdate_ = thisUpdate;
[self setHoverAlpha:opacity];
[[self controlView] setNeedsDisplay:YES];
// If our state needs it, keep going.
if ([self pulsing]) {
[self performSelector:_cmd withObject:nil afterDelay:0.02];
}
}
- (gradient_button_cell::PulseState)pulseState {
return pulseState_;
}
// Set the pulsing state. This can either set the pulse to on or off
// immediately (e.g. kPulsedOn, kPulsedOff) or initiate an animated
// state change.
- (void)setPulseState:(gradient_button_cell::PulseState)pstate {
pulseState_ = pstate;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate];
switch (pstate) {
case gradient_button_cell::kPulsedOn:
case gradient_button_cell::kPulsedOff:
outerStrokeAlphaMult_ = 1.0;
[self setHoverAlpha:((pulseState_ == gradient_button_cell::kPulsedOn) ?
1.0 : 0.0)];
[[self controlView] setNeedsDisplay:YES];
break;
case gradient_button_cell::kPulsingOn:
case gradient_button_cell::kPulsingOff:
outerStrokeAlphaMult_ = 1.0;
// Set initial value then engage timer.
[self setHoverAlpha:((pulseState_ == gradient_button_cell::kPulsingOn) ?
0.0 : 1.0)];
[self performOnePulseStep];
break;
case gradient_button_cell::kPulsingStuckOn:
// Semantics of continuous pulsing are that we pulse independent
// of mouse position.
[self performOnePulseStep];
break;
default:
CHECK(0);
break;
}
}
- (void)safelyStopPulsing {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void)setPulseIsStuckOn:(BOOL)on {
if (!on && pulseState_ != gradient_button_cell::kPulsingStuckOn)
return;
if (on) {
[self setPulseState:gradient_button_cell::kPulsingStuckOn];
} else {
[self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsedOn :
gradient_button_cell::kPulsedOff)];
}
}
- (BOOL)isPulseStuckOn {
return (pulseState_ == gradient_button_cell::kPulsingStuckOn) ?
YES : NO;
}
#if 1
// If we are not continuously pulsing, perform a pulse animation to
// reflect our new state.
- (void)setMouseInside:(BOOL)flag animate:(BOOL)animated {
isMouseInside_ = flag;
if (pulseState_ != gradient_button_cell::kPulsingStuckOn) {
if (animated) {
// In Material Design, if the button is already fully on, don't pulse it
// on again if the mouse is within its bounds.
if ([self tag] == [self isMaterialDesignButtonType] &&
isMouseInside_ && pulseState_ == gradient_button_cell::kPulsedOn) {
return;
}
[self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsingOn :
gradient_button_cell::kPulsingOff)];
} else {
[self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsedOn :
gradient_button_cell::kPulsedOff)];
}
}
}
#else
- (void)adjustHoverValue {
NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
CGFloat opacity = [self hoverAlpha];
if (isMouseInside_) {
opacity += elapsed / kAnimationShowDuration;
} else {
opacity -= elapsed / kAnimationHideDuration;
}
if (!isMouseInside_ && opacity < 0) {
opacity = 0;
} else if (isMouseInside_ && opacity > 1) {
opacity = 1;
} else {
[self performSelector:_cmd withObject:nil afterDelay:0.02];
}
lastHoverUpdate_ = thisUpdate;
[self setHoverAlpha:opacity];
[[self controlView] setNeedsDisplay:YES];
}
- (void)setMouseInside:(BOOL)flag animate:(BOOL)animated {
isMouseInside_ = flag;
if (animated) {
lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate];
[self adjustHoverValue];
} else {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self setHoverAlpha:flag ? 1.0 : 0.0];
}
[[self controlView] setNeedsDisplay:YES];
}
#endif
- (NSGradient*)gradientForHoverAlpha:(CGFloat)hoverAlpha
isThemed:(BOOL)themed {
CGFloat startAlpha = 0.6 + 0.3 * hoverAlpha;
CGFloat endAlpha = 0.333 * hoverAlpha;
if (themed) {
startAlpha = 0.2 + 0.35 * hoverAlpha;
endAlpha = 0.333 * hoverAlpha;
}
NSColor* startColor =
[NSColor colorWithCalibratedWhite:1.0
alpha:startAlpha];
NSColor* endColor =
[NSColor colorWithCalibratedWhite:1.0 - 0.15 * hoverAlpha
alpha:endAlpha];
NSGradient* gradient = [[NSGradient alloc] initWithColorsAndLocations:
startColor, hoverAlpha * 0.33,
endColor, 1.0, nil];
return [gradient autorelease];
}
- (void)sharedInit {
shouldTheme_ = YES;
pulseState_ = gradient_button_cell::kPulsedOff;
outerStrokeAlphaMult_ = 1.0;
gradient_.reset([[self gradientForHoverAlpha:0.0 isThemed:NO] retain]);
}
- (void)setShouldTheme:(BOOL)shouldTheme {
shouldTheme_ = shouldTheme;
}
- (NSBackgroundStyle)interiorBackgroundStyle {
// Never lower the interior, since that just leads to a weird shadow which can
// often interact badly with the theme.
return NSBackgroundStyleRaised;
}
- (void)mouseEntered:(NSEvent*)theEvent {
[self setMouseInside:YES animate:YES];
}
- (void)mouseExited:(NSEvent*)theEvent {
[self setMouseInside:NO animate:YES];
}
- (BOOL)isMouseInside {
return trackingArea_ && isMouseInside_;
}
- (BOOL)startTrackingAt:(NSPoint)startPoint inView:(NSView *)controlView {
if ([self isMaterialDesignButtonType]) {
// The user has just clicked down in the button. In Material Design, set the
// pulsed (hover) state to off now so that if the user keeps the mouse held
// down while dragging it out of the button's bounds, the button will draw
// itself in its normal state. This is unrelated to dragging the button
// in the button bar, which takes a different path through the code.
[self setPulseState:gradient_button_cell::kPulsedOff];
}
return [super startTrackingAt:startPoint inView:controlView];
}
// Since we have our own drawWithFrame:, we need to also have our own
// logic for determining when the mouse is inside for honoring this
// request.
- (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
[super setShowsBorderOnlyWhileMouseInside:showOnly];
if (showOnly) {
[self updateTrackingAreas];
} else {
if (trackingArea_) {
[[self controlView] removeTrackingArea:trackingArea_];
trackingArea_.reset(nil);
if (isMouseInside_) {
isMouseInside_ = NO;
[[self controlView] setNeedsDisplay:YES];
}
}
}
}
// TODO(viettrungluu): clean up/reorganize.
- (void)drawBorderAndFillForTheme:(const ui::ThemeProvider*)themeProvider
controlView:(NSView*)controlView
innerPath:(NSBezierPath*)innerPath
showClickedGradient:(BOOL)showClickedGradient
showHighlightGradient:(BOOL)showHighlightGradient
hoverAlpha:(CGFloat)hoverAlpha
active:(BOOL)active
cellFrame:(NSRect)cellFrame
defaultGradient:(NSGradient*)defaultGradient {
// For Material Design, draw a solid rounded rect behind the button, based on
// the hover and pressed states.
if ([self isMaterialDesignButtonType]) {
const CGFloat kEightPercentAlpha = 0.08;
const CGFloat kFourPercentAlpha = 0.04;
// The alpha is always at least 8%. Default the color to black.
CGFloat alpha = kEightPercentAlpha;
CGFloat color = 0.0;
// If a dark theme, increase the opacity slightly and use white.
if ([[controlView window] hasDarkTheme]) {
alpha += kFourPercentAlpha;
color = 1.0;
}
// If clicked or highlighted, the background is slightly more opaque. If not
// clicked or highlighted, adjust the alpha by the animation fade in
// percentage.
if (showClickedGradient || showHighlightGradient) {
alpha += kFourPercentAlpha;
} else {
alpha *= hoverAlpha;
}
// Fill the path.
[[NSColor colorWithCalibratedWhite:color alpha:alpha] set];
[innerPath fill];
return;
}
BOOL isFlatButton = [self showsBorderOnlyWhileMouseInside];
// For flat (unbordered when not hovered) buttons, never use the toolbar
// button background image, but the modest gradient used for themed buttons.
// To make things even more modest, scale the hover alpha down by 40 percent
// unless clicked.
NSColor* backgroundImageColor;
BOOL useThemeGradient;
if (isFlatButton) {
backgroundImageColor = nil;
useThemeGradient = YES;
if (!showClickedGradient)
hoverAlpha *= 0.6;
} else {
backgroundImageColor = nil;
if (themeProvider &&
themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND)) {
backgroundImageColor =
themeProvider->GetNSImageColorNamed(IDR_THEME_BUTTON_BACKGROUND);
}
useThemeGradient = backgroundImageColor ? YES : NO;
}
// The basic gradient shown inside; see above.
NSGradient* gradient;
if (hoverAlpha == 0 && !useThemeGradient) {
gradient = defaultGradient ? defaultGradient : gradient_.get();
} else {
gradient = [self gradientForHoverAlpha:hoverAlpha
isThemed:useThemeGradient];
}
// If we're drawing a background image, show that; else possibly show the
// clicked gradient.
if (backgroundImageColor) {
[backgroundImageColor set];
// Set the phase to match window.
NSRect trueRect = [controlView convertRect:cellFrame toView:nil];
[[NSGraphicsContext currentContext]
cr_setPatternPhase:NSMakePoint(NSMinX(trueRect), NSMaxY(trueRect))
forView:controlView];
[innerPath fill];
} else {
if (showClickedGradient) {
NSGradient* clickedGradient = nil;
if (isFlatButton &&
[self tag] == kStandardButtonTypeWithLimitedClickFeedback) {
clickedGradient = gradient;
} else {
clickedGradient = themeProvider ? themeProvider->GetNSGradient(
active ?
ThemeProperties::GRADIENT_TOOLBAR_BUTTON_PRESSED :
ThemeProperties::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE) :
nil;
}
[clickedGradient drawInBezierPath:innerPath angle:90.0];
}
}
// Visually indicate unclicked, enabled buttons.
if (!showClickedGradient && [self isEnabled]) {
gfx::ScopedNSGraphicsContextSaveGState scopedGState;
[innerPath addClip];
// Draw the inner glow.
if (hoverAlpha > 0) {
[innerPath setLineWidth:2];
[[NSColor colorWithCalibratedWhite:1.0 alpha:0.2 * hoverAlpha] setStroke];
[innerPath stroke];
}
// Draw the top inner highlight.
NSAffineTransform* highlightTransform = [NSAffineTransform transform];
[highlightTransform translateXBy:1 yBy:1];
base::scoped_nsobject<NSBezierPath> highlightPath([innerPath copy]);
[highlightPath transformUsingAffineTransform:highlightTransform];
[[NSColor colorWithCalibratedWhite:1.0 alpha:0.2] setStroke];
[highlightPath stroke];
// Draw the gradient inside.
[gradient drawInBezierPath:innerPath angle:90.0];
}
// Don't draw anything else for disabled flat buttons.
if (isFlatButton && ![self isEnabled])
return;
// Draw the outer stroke.
NSColor* strokeColor = nil;
if (showClickedGradient) {
strokeColor = [NSColor
colorWithCalibratedWhite:0.0
alpha:0.3 * outerStrokeAlphaMult_];
} else {
strokeColor = themeProvider ? themeProvider->GetNSColor(
active ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE :
ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) :
[NSColor colorWithCalibratedWhite:0.0
alpha:0.3 * outerStrokeAlphaMult_];
}
[strokeColor setStroke];
[innerPath setLineWidth:1];
[innerPath stroke];
}
// TODO(viettrungluu): clean this up.
// (Private)
- (void)getDrawParamsForFrame:(NSRect)cellFrame
inView:(NSView*)controlView
innerFrame:(NSRect*)returnInnerFrame
innerPath:(NSBezierPath**)returnInnerPath
clipPath:(NSBezierPath**)returnClipPath {
const CGFloat lineWidth = [[self class] insetInView:controlView];
const CGFloat halfLineWidth = lineWidth / 2.0;
NSRect drawFrame = NSZeroRect;
NSRect innerFrame = NSZeroRect;
CGFloat cornerRadius = 2;
if (![self isMaterialDesignButtonType]) {
drawFrame = NSInsetRect(cellFrame, 1.5 * lineWidth, 1.5 * lineWidth);
innerFrame = NSInsetRect(cellFrame, lineWidth, lineWidth);
cornerRadius = 3;
} else {
drawFrame = cellFrame;
// Hover and click paths are always 20pt tall, regardless of the button's
// height.
drawFrame.size.height = 20;
innerFrame = NSInsetRect(drawFrame, lineWidth, lineWidth);
}
ButtonType type = [[(NSControl*)controlView cell] tag];
switch (type) {
case kMiddleButtonType:
drawFrame.size.width += 20;
innerFrame.size.width += 2;
FALLTHROUGH;
case kRightButtonType:
drawFrame.origin.x -= 20;
innerFrame.origin.x -= 2;
FALLTHROUGH;
case kLeftButtonType:
case kLeftButtonWithShadowType:
drawFrame.size.width += 20;
innerFrame.size.width += 2;
break;
default:
break;
}
if (type == kLeftButtonWithShadowType)
innerFrame.size.width -= 1.0;
// Return results if |return...| not null.
if (returnInnerFrame)
*returnInnerFrame = innerFrame;
if (returnInnerPath) {
DCHECK(*returnInnerPath == nil);
drawFrame.origin.y +=
[self hoverBackgroundVerticalOffsetInControlView:controlView];
if ([self tag] == kMaterialMenuButtonTypeWithLimitedClickFeedback) {
*returnInnerPath = [NSBezierPath bezierPathWithRect:drawFrame];
} else {
*returnInnerPath = [NSBezierPath bezierPathWithRoundedRect:drawFrame
xRadius:cornerRadius
yRadius:cornerRadius];
}
[*returnInnerPath setLineWidth:lineWidth];
}
if (returnClipPath) {
DCHECK(*returnClipPath == nil);
NSRect clipPathRect =
NSInsetRect(drawFrame, -halfLineWidth, -halfLineWidth);
*returnClipPath =
[NSBezierPath bezierPathWithRoundedRect:clipPathRect
xRadius:cornerRadius + halfLineWidth
yRadius:cornerRadius + halfLineWidth];
}
}
// TODO(viettrungluu): clean this up.
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
NSRect innerFrame;
NSBezierPath* innerPath = nil;
[self getDrawParamsForFrame:cellFrame
inView:controlView
innerFrame:&innerFrame
innerPath:&innerPath
clipPath:NULL];
BOOL enabled = [((NSControl*)[self controlView]) isEnabled];
BOOL pressed = enabled && [self isHighlighted];
NSWindow* window = [controlView window];
const ui::ThemeProvider* themeProvider = [window themeProvider];
BOOL active = [window isKeyWindow] || [window isMainWindow];
// Stroke the borders and appropriate fill gradient. If we're borderless, the
// only time we want to draw the inner gradient is if we're highlighted or if
// we're drawing the focus ring manually. In Material Design, the "border" is
// actually a highlight, which should be drawn if
// |showsBorderOnlyWhileMouseInside| is true.
BOOL hasMaterialHighlight =
[self isMaterialDesignButtonType] &&
![self showsBorderOnlyWhileMouseInside] &&
enabled;
if (([self isBordered] && ![self showsBorderOnlyWhileMouseInside]) ||
pressed || [self isMouseInside] || [self isPulseStuckOn] ||
hasMaterialHighlight) {
// When pulsing we want the bookmark to stand out a little more.
BOOL showClickedGradient = pressed ||
(pulseState_ == gradient_button_cell::kPulsingStuckOn);
BOOL showHighlightGradient = [self isHighlighted] || hasMaterialHighlight;
[self drawBorderAndFillForTheme:themeProvider
controlView:controlView
innerPath:innerPath
showClickedGradient:showClickedGradient
showHighlightGradient:showHighlightGradient
hoverAlpha:[self hoverAlpha]
active:active
cellFrame:cellFrame
defaultGradient:nil];
}
// If this is the left side of a segmented button, draw a slight shadow.
ButtonType type = [[(NSControl*)controlView cell] tag];
if (type == kLeftButtonWithShadowType) {
const CGFloat lineWidth = [controlView cr_lineWidth];
NSRect borderRect, contentRect;
NSDivideRect(cellFrame, &borderRect, &contentRect, lineWidth, NSMaxXEdge);
NSColor* stroke = themeProvider ? themeProvider->GetNSColor(
active ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE :
ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) :
[NSColor blackColor];
[[stroke colorWithAlphaComponent:0.2] set];
NSRectFillUsingOperation(NSInsetRect(borderRect, 0, 2),
NSCompositeSourceOver);
}
[self drawInteriorWithFrame:innerFrame inView:controlView];
}
- (CGFloat)textStartXOffset {
// 11 is the magic number needed to make this match the native
// NSButtonCell's label display.
return [[self image] size].width + 11;
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
const CGFloat lineWidth = [controlView cr_lineWidth];
if (shouldTheme_) {
BOOL isTemplate = [[self image] isTemplate];
gfx::ScopedNSGraphicsContextSaveGState scopedGState;
CGContextRef context =
(CGContextRef)([[NSGraphicsContext currentContext] graphicsPort]);
const ui::ThemeProvider* themeProvider =
[[controlView window] themeProvider];
NSColor* color = themeProvider ?
themeProvider->GetNSColorTint(ThemeProperties::TINT_BUTTONS) :
[NSColor blackColor];
if (isTemplate && themeProvider && themeProvider->UsingSystemTheme()) {
base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
[shadow.get() setShadowColor:themeProvider->GetNSColor(
ThemeProperties::COLOR_TOOLBAR_BEZEL)];
[shadow.get() setShadowOffset:NSMakeSize(0.0, -lineWidth)];
[shadow setShadowBlurRadius:lineWidth];
[shadow set];
}
CGContextBeginTransparencyLayer(context, 0);
NSRect imageRect = NSZeroRect;
imageRect.size = [[self image] size];
NSRect drawRect = [self imageRectForBounds:cellFrame];
[[self image] drawInRect:drawRect
fromRect:imageRect
operation:NSCompositeSourceOver
fraction:[self isEnabled] ? 1.0 : 0.5
respectFlipped:YES
hints:nil];
if (isTemplate && color) {
[color set];
NSRectFillUsingOperation(cellFrame, NSCompositeSourceAtop);
}
CGContextEndTransparencyLayer(context);
} else {
// NSCell draws these off-center for some reason, probably because of the
// positioning of the control in the xib.
[super drawInteriorWithFrame:NSOffsetRect(cellFrame, 0, lineWidth)
inView:controlView];
}
}
- (int)verticalTextOffset {
return 1;
}
- (CGFloat)hoverBackgroundVerticalOffsetInControlView:(NSView*)controlView {
return 0.0;
}
- (BOOL)isMaterialDesignButtonType {
return [self tag] == kMaterialStandardButtonTypeWithLimitedClickFeedback ||
[self tag] == kMaterialMenuButtonTypeWithLimitedClickFeedback;
}
// Overriden from NSButtonCell so we can display a nice fadeout effect for
// button titles that overflow.
// This method is copied in the most part from GTMFadeTruncatingTextFieldCell,
// the only difference is that here we draw the text ourselves rather than
// calling the super to do the work.
// We can't use GTMFadeTruncatingTextFieldCell because there's no easy way to
// get it to work with NSButtonCell.
// TODO(jeremy): Move this to GTM.
- (NSRect)drawTitle:(NSAttributedString*)title
withFrame:(NSRect)cellFrame
inView:(NSView*)controlView {
NSSize size = [title size];
// Empirically, Cocoa will draw an extra 2 pixels past NSWidth(cellFrame)
// before it clips the text.
const CGFloat kOverflowBeforeClip = 2;
BOOL isModeMaterial = [self isMaterialDesignButtonType];
// For Material Design we don't want to clip the text. For all other button
// cell modes, we do.
BOOL shouldClipTheTitle = !isModeMaterial;
if (std::floor(size.width) <= (NSWidth(cellFrame) + kOverflowBeforeClip)) {
cellFrame.origin.y += ([self verticalTextOffset] - 1);
shouldClipTheTitle = NO;
}
// Gradient is about twice our line height long.
CGFloat gradientWidth = MIN(size.height * 2, NSWidth(cellFrame) / 4);
NSRect solidPart, gradientPart;
NSDivideRect(cellFrame, &gradientPart, &solidPart, gradientWidth, NSMaxXEdge);
// Draw non-gradient part without transparency layer, as light text on a dark
// background looks bad with a gradient layer.
NSPoint textOffset = NSZeroPoint;
{
gfx::ScopedNSGraphicsContextSaveGState scopedGState;
if (shouldClipTheTitle)
[NSBezierPath clipRect:solidPart];
// For some reason, the height of cellFrame as passed in is totally bogus.
// For vertical centering purposes, we need the bounds of the containing
// view.
NSRect buttonFrame = [[self controlView] frame];
// Call the vertical offset to match native NSButtonCell's version.
textOffset = NSMakePoint(
NSMinX(cellFrame),
(NSHeight(buttonFrame) - size.height) / 2 + [self verticalTextOffset]);
// WIth Material Design we want an ellipsis if the title is too long to fit,
// so have to use drawInRect: instead of drawAtPoint:.
if (isModeMaterial) {
NSRect textFrame = NSMakeRect(textOffset.x, textOffset.y,
NSWidth(cellFrame), NSHeight(buttonFrame));
[title drawInRect:textFrame];
} else {
[title drawAtPoint:textOffset];
}
}
if (!shouldClipTheTitle)
return cellFrame;
// Draw the gradient part with a transparency layer. This makes the text look
// suboptimal, but since it fades out, that's ok.
gfx::ScopedNSGraphicsContextSaveGState scopedGState;
[NSBezierPath clipRect:gradientPart];
CGContextRef context = static_cast<CGContextRef>(
[[NSGraphicsContext currentContext] graphicsPort]);
CGContextBeginTransparencyLayerWithRect(context,
NSRectToCGRect(gradientPart), 0);
[title drawAtPoint:textOffset];
NSColor *color = [NSColor textColor];
NSColor *alphaColor = [color colorWithAlphaComponent:0.0];
NSGradient *mask = [[NSGradient alloc] initWithStartingColor:color
endingColor:alphaColor];
// Draw the gradient mask
CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
[mask drawFromPoint:NSMakePoint(NSMaxX(cellFrame) - gradientWidth,
NSMinY(cellFrame))
toPoint:NSMakePoint(NSMaxX(cellFrame),
NSMinY(cellFrame))
options:NSGradientDrawsBeforeStartingLocation];
[mask release];
CGContextEndTransparencyLayer(context);
return cellFrame;
}
- (NSBezierPath*)clipPathForFrame:(NSRect)cellFrame
inView:(NSView*)controlView {
NSBezierPath* boundingPath = nil;
[self getDrawParamsForFrame:cellFrame
inView:controlView
innerFrame:NULL
innerPath:NULL
clipPath:&boundingPath];
return boundingPath;
}
- (void)resetCursorRect:(NSRect)cellFrame inView:(NSView*)controlView {
[super resetCursorRect:cellFrame inView:controlView];
if (trackingArea_)
[self updateTrackingAreas];
}
- (BOOL)isMouseReallyInside {
BOOL mouseInView = NO;
NSView* controlView = [self controlView];
NSWindow* window = [controlView window];
NSRect bounds = [controlView bounds];
if (window) {
NSPoint mousePoint = [window mouseLocationOutsideOfEventStream];
mousePoint = [controlView convertPoint:mousePoint fromView:nil];
mouseInView = [controlView mouse:mousePoint inRect:bounds];
}
return mouseInView;
}
- (void)updateTrackingAreas {
NSView* controlView = [self controlView];
BOOL mouseInView = [self isMouseReallyInside];
if (trackingArea_.get())
[controlView removeTrackingArea:trackingArea_];
NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
NSTrackingActiveInActiveApp;
if (mouseInView)
options |= NSTrackingAssumeInside;
trackingArea_.reset([[NSTrackingArea alloc]
initWithRect:[controlView bounds]
options:options
owner:self
userInfo:nil]);
if (isMouseInside_ != mouseInView) {
[self setMouseInside:mouseInView animate:NO];
[controlView setNeedsDisplay:YES];
}
}
@end