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/hover_close_button.h"
6
7#include "chrome/grit/generated_resources.h"
8#include "grit/theme_resources.h"
9#import "third_party/google_toolbox_for_mac/src/AppKit/GTMKeyValueAnimation.h"
10#include "ui/base/cocoa/animation_utils.h"
11#include "ui/base/l10n/l10n_util.h"
12#include "ui/base/resource/resource_bundle.h"
13#include "ui/resources/grit/ui_resources.h"
14
15namespace  {
16const CGFloat kFramesPerSecond = 16; // Determined experimentally to look good.
17const CGFloat kCloseAnimationDuration = 0.1;
18
19// Strings that are used for all close buttons. Set up in +initialize.
20NSString* gTooltip = nil;
21NSString* gDescription = nil;
22
23// If this string is changed, the setter (currently setFadeOutValue:) must
24// be changed as well to match.
25NSString* const kFadeOutValueKeyPath = @"fadeOutValue";
26}  // namespace
27
28@interface HoverCloseButton ()
29
30// Common initialization routine called from initWithFrame and awakeFromNib.
31- (void)commonInit;
32
33// Called by |fadeOutAnimation_| when animated value changes.
34- (void)setFadeOutValue:(CGFloat)value;
35
36// Gets the image for the given hover state.
37- (NSImage*)imageForHoverState:(HoverState)hoverState;
38
39@end
40
41@implementation HoverCloseButton
42
43+ (void)initialize {
44  // Grab some strings that are used by all close buttons.
45  if (!gDescription)
46    gDescription = [l10n_util::GetNSStringWithFixup(IDS_ACCNAME_CLOSE) copy];
47  if (!gTooltip)
48    gTooltip = [l10n_util::GetNSStringWithFixup(IDS_TOOLTIP_CLOSE_TAB) copy];
49}
50
51- (id)initWithFrame:(NSRect)frameRect {
52  if ((self = [super initWithFrame:frameRect])) {
53    [self commonInit];
54  }
55  return self;
56}
57
58- (void)awakeFromNib {
59  [super awakeFromNib];
60  [self commonInit];
61}
62
63- (void)removeFromSuperview {
64  // -stopAnimation will call the animationDidStop: delegate method
65  // which will release our animation.
66  [fadeOutAnimation_ stopAnimation];
67  [super removeFromSuperview];
68}
69
70- (void)animationDidStop:(NSAnimation*)animation {
71  DCHECK(animation == fadeOutAnimation_);
72  [fadeOutAnimation_ setDelegate:nil];
73  [fadeOutAnimation_ release];
74  fadeOutAnimation_ = nil;
75}
76
77- (void)animationDidEnd:(NSAnimation*)animation {
78  [self animationDidStop:animation];
79}
80
81- (void)drawRect:(NSRect)dirtyRect {
82  NSImage* image = [self imageForHoverState:[self hoverState]];
83
84  // Close boxes align left horizontally, and align center vertically.
85  // http:crbug.com/14739 requires this.
86  NSRect imageRect = NSZeroRect;
87  imageRect.size = [image size];
88
89  NSRect destRect = [self bounds];
90  destRect.origin.y = floor((NSHeight(destRect) / 2)
91                            - (NSHeight(imageRect) / 2));
92  destRect.size = imageRect.size;
93
94  switch(self.hoverState) {
95    case kHoverStateMouseOver:
96      [image drawInRect:destRect
97               fromRect:imageRect
98              operation:NSCompositeSourceOver
99               fraction:1.0
100         respectFlipped:YES
101                  hints:nil];
102      break;
103
104    case kHoverStateMouseDown:
105      [image drawInRect:destRect
106               fromRect:imageRect
107              operation:NSCompositeSourceOver
108               fraction:1.0
109         respectFlipped:YES
110                  hints:nil];
111      break;
112
113    case kHoverStateNone: {
114      CGFloat value = 1.0;
115      if (fadeOutAnimation_) {
116        value = [fadeOutAnimation_ currentValue];
117        NSImage* previousImage = nil;
118        if (previousState_ == kHoverStateMouseOver)
119          previousImage = [self imageForHoverState:kHoverStateMouseOver];
120        else
121          previousImage = [self imageForHoverState:kHoverStateMouseDown];
122        [previousImage drawInRect:destRect
123                         fromRect:imageRect
124                        operation:NSCompositeSourceOver
125                         fraction:1.0 - value
126                   respectFlipped:YES
127                            hints:nil];
128      }
129      [image drawInRect:destRect
130               fromRect:imageRect
131              operation:NSCompositeSourceOver
132               fraction:value
133         respectFlipped:YES
134                  hints:nil];
135      break;
136    }
137  }
138}
139
140- (void)setFadeOutValue:(CGFloat)value {
141  [self setNeedsDisplay];
142}
143
144- (NSImage*)imageForHoverState:(HoverState)hoverState {
145  int imageID = IDR_CLOSE_1;
146  switch (hoverState) {
147    case kHoverStateNone:
148      imageID = IDR_CLOSE_1;
149      break;
150    case kHoverStateMouseOver:
151      imageID = IDR_CLOSE_1_H;
152      break;
153    case kHoverStateMouseDown:
154      imageID = IDR_CLOSE_1_P;
155      break;
156  }
157  ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
158  return bundle.GetNativeImageNamed(imageID).ToNSImage();
159}
160
161- (void)setHoverState:(HoverState)state {
162  if (state != self.hoverState) {
163    previousState_ = self.hoverState;
164    [super setHoverState:state];
165    // Only animate the HoverStateNone case.
166    if (state == kHoverStateNone) {
167      DCHECK(fadeOutAnimation_ == nil);
168      fadeOutAnimation_ =
169          [[GTMKeyValueAnimation alloc] initWithTarget:self
170                                               keyPath:kFadeOutValueKeyPath];
171      [fadeOutAnimation_ setDuration:kCloseAnimationDuration];
172      [fadeOutAnimation_ setFrameRate:kFramesPerSecond];
173      [fadeOutAnimation_ setDelegate:self];
174      [fadeOutAnimation_ startAnimation];
175    } else {
176      // -stopAnimation will call the animationDidStop: delegate method
177      // which will clean up the animation.
178      [fadeOutAnimation_ stopAnimation];
179    }
180  }
181}
182
183- (void)commonInit {
184  // Set accessibility description.
185  NSCell* cell = [self cell];
186  [cell accessibilitySetOverrideValue:gDescription
187                         forAttribute:NSAccessibilityTitleAttribute];
188
189  // Add a tooltip. Using 'owner:self' means that
190  // -view:stringForToolTip:point:userData: will be called to provide the
191  // tooltip contents immediately before showing it.
192  [self addToolTipRect:[self bounds] owner:self userData:NULL];
193
194  // Initialize previousState.
195  previousState_ = kHoverStateNone;
196}
197
198// Called each time a tooltip is about to be shown.
199- (NSString*)view:(NSView*)view
200 stringForToolTip:(NSToolTipTag)tag
201            point:(NSPoint)point
202         userData:(void*)userData {
203  if (self.hoverState == kHoverStateMouseOver) {
204    // In some cases (e.g. the download tray), the button is still in the
205    // hover state, but is outside the bounds of its parent and not visible.
206    // Don't show the tooltip in that case.
207    NSRect buttonRect = [self frame];
208    NSRect parentRect = [[self superview] bounds];
209    if (NSIntersectsRect(buttonRect, parentRect))
210      return gTooltip;
211  }
212
213  return nil;  // Do not show the tooltip.
214}
215
216@end
217
218@implementation WebUIHoverCloseButton
219
220- (NSImage*)imageForHoverState:(HoverState)hoverState {
221  int imageID = IDR_CLOSE_DIALOG;
222  switch (hoverState) {
223    case kHoverStateNone:
224      imageID = IDR_CLOSE_DIALOG;
225      break;
226    case kHoverStateMouseOver:
227      imageID = IDR_CLOSE_DIALOG_H;
228      break;
229    case kHoverStateMouseDown:
230      imageID = IDR_CLOSE_DIALOG_P;
231      break;
232  }
233  ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
234  return bundle.GetNativeImageNamed(imageID).ToNSImage();
235}
236
237@end
238