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/dock_icon.h"
6
7#include "base/logging.h"
8#include "base/mac/bundle_locations.h"
9#include "base/mac/scoped_nsobject.h"
10#include "content/public/browser/browser_thread.h"
11#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
12
13using content::BrowserThread;
14
15namespace {
16
17// The fraction of the size of the dock icon that the badge is.
18const float kBadgeFraction = 0.4f;
19
20// The indentation of the badge.
21const float kBadgeIndent = 5.0f;
22
23// The maximum update rate for the dock icon. 200ms = 5fps.
24const int64 kUpdateFrequencyMs = 200;
25
26}  // namespace
27
28// A view that draws our dock tile.
29@interface DockTileView : NSView {
30 @private
31  int downloads_;
32  BOOL indeterminate_;
33  float progress_;
34}
35
36// Indicates how many downloads are in progress.
37@property (nonatomic) int downloads;
38
39// Indicates whether the progress indicator should be in an indeterminate state
40// or not.
41@property (nonatomic) BOOL indeterminate;
42
43// Indicates the amount of progress made of the download. Ranges from [0..1].
44@property (nonatomic) float progress;
45
46@end
47
48@implementation DockTileView
49
50@synthesize downloads = downloads_;
51@synthesize indeterminate = indeterminate_;
52@synthesize progress = progress_;
53
54- (void)drawRect:(NSRect)dirtyRect {
55  // Not -[NSApplication applicationIconImage]; that fails to return a pasted
56  // custom icon.
57  NSString* appPath = [base::mac::MainBundle() bundlePath];
58  NSImage* appIcon = [[NSWorkspace sharedWorkspace] iconForFile:appPath];
59  [appIcon drawInRect:[self bounds]
60             fromRect:NSZeroRect
61            operation:NSCompositeSourceOver
62             fraction:1.0];
63
64  if (downloads_ == 0)
65    return;
66
67  NSRect badgeRect = [self bounds];
68  badgeRect.size.height = (int)(kBadgeFraction * badgeRect.size.height);
69  int newWidth = kBadgeFraction * NSWidth(badgeRect);
70  badgeRect.origin.x = NSWidth(badgeRect) - newWidth;
71  badgeRect.size.width = newWidth;
72
73  CGFloat badgeRadius = NSMidY(badgeRect);
74
75  badgeRect.origin.x -= kBadgeIndent;
76  badgeRect.origin.y += kBadgeIndent;
77
78  NSPoint badgeCenter = NSMakePoint(NSMidX(badgeRect), NSMidY(badgeRect));
79
80  // Background
81  NSColor* backgroundColor = [NSColor colorWithCalibratedRed:0.85
82                                                       green:0.85
83                                                        blue:0.85
84                                                       alpha:1.0];
85  NSColor* backgroundHighlight =
86      [backgroundColor blendedColorWithFraction:0.85
87                                        ofColor:[NSColor whiteColor]];
88  base::scoped_nsobject<NSGradient> backgroundGradient(
89      [[NSGradient alloc] initWithStartingColor:backgroundHighlight
90                                    endingColor:backgroundColor]);
91  NSBezierPath* badgeEdge = [NSBezierPath bezierPathWithOvalInRect:badgeRect];
92  {
93    gfx::ScopedNSGraphicsContextSaveGState scopedGState;
94    [badgeEdge addClip];
95    [backgroundGradient drawFromCenter:badgeCenter
96                                radius:0.0
97                              toCenter:badgeCenter
98                                radius:badgeRadius
99                               options:0];
100  }
101
102  // Slice
103  if (!indeterminate_) {
104    NSColor* sliceColor = [NSColor colorWithCalibratedRed:0.45
105                                                    green:0.8
106                                                     blue:0.25
107                                                    alpha:1.0];
108    NSColor* sliceHighlight =
109        [sliceColor blendedColorWithFraction:0.4
110                                     ofColor:[NSColor whiteColor]];
111    base::scoped_nsobject<NSGradient> sliceGradient(
112        [[NSGradient alloc] initWithStartingColor:sliceHighlight
113                                      endingColor:sliceColor]);
114    NSBezierPath* progressSlice;
115    if (progress_ >= 1.0) {
116      progressSlice = [NSBezierPath bezierPathWithOvalInRect:badgeRect];
117    } else {
118      CGFloat endAngle = 90.0 - 360.0 * progress_;
119      if (endAngle < 0.0)
120        endAngle += 360.0;
121      progressSlice = [NSBezierPath bezierPath];
122      [progressSlice moveToPoint:badgeCenter];
123      [progressSlice appendBezierPathWithArcWithCenter:badgeCenter
124                                                radius:badgeRadius
125                                            startAngle:90.0
126                                              endAngle:endAngle
127                                             clockwise:YES];
128      [progressSlice closePath];
129    }
130    gfx::ScopedNSGraphicsContextSaveGState scopedGState;
131    [progressSlice addClip];
132    [sliceGradient drawFromCenter:badgeCenter
133                           radius:0.0
134                         toCenter:badgeCenter
135                           radius:badgeRadius
136                          options:0];
137  }
138
139  // Edge
140  {
141    gfx::ScopedNSGraphicsContextSaveGState scopedGState;
142    [[NSColor whiteColor] set];
143    base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
144    [shadow.get() setShadowOffset:NSMakeSize(0, -2)];
145    [shadow setShadowBlurRadius:2];
146    [shadow set];
147    [badgeEdge setLineWidth:2];
148    [badgeEdge stroke];
149  }
150
151  // Download count
152  base::scoped_nsobject<NSNumberFormatter> formatter(
153      [[NSNumberFormatter alloc] init]);
154  NSString* countString =
155      [formatter stringFromNumber:[NSNumber numberWithInt:downloads_]];
156
157  base::scoped_nsobject<NSShadow> countShadow([[NSShadow alloc] init]);
158  [countShadow setShadowBlurRadius:3.0];
159  [countShadow.get() setShadowColor:[NSColor whiteColor]];
160  [countShadow.get() setShadowOffset:NSMakeSize(0.0, 0.0)];
161  NSMutableDictionary* countAttrsDict =
162      [NSMutableDictionary dictionaryWithObjectsAndKeys:
163          [NSColor blackColor], NSForegroundColorAttributeName,
164          countShadow.get(), NSShadowAttributeName,
165          nil];
166  CGFloat countFontSize = badgeRadius;
167  NSSize countSize = NSZeroSize;
168  base::scoped_nsobject<NSAttributedString> countAttrString;
169  while (1) {
170    NSFont* countFont = [NSFont fontWithName:@"Helvetica-Bold"
171                                        size:countFontSize];
172
173    // This will generally be plain Helvetica.
174    if (!countFont)
175      countFont = [NSFont userFontOfSize:countFontSize];
176
177    // Continued failure would generate an NSException.
178    if (!countFont)
179      break;
180
181    [countAttrsDict setObject:countFont forKey:NSFontAttributeName];
182    countAttrString.reset(
183        [[NSAttributedString alloc] initWithString:countString
184                                        attributes:countAttrsDict]);
185    countSize = [countAttrString size];
186    if (countSize.width > badgeRadius * 1.5) {
187      countFontSize -= 1.0;
188    } else {
189      break;
190    }
191  }
192
193  NSPoint countOrigin = badgeCenter;
194  countOrigin.x -= countSize.width / 2;
195  countOrigin.y -= countSize.height / 2.2;  // tweak; otherwise too low
196
197  [countAttrString.get() drawAtPoint:countOrigin];
198}
199
200@end
201
202
203@implementation DockIcon
204
205+ (DockIcon*)sharedDockIcon {
206  static DockIcon* icon;
207  if (!icon) {
208    NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
209
210    base::scoped_nsobject<DockTileView> dockTileView(
211        [[DockTileView alloc] init]);
212    [dockTile setContentView:dockTileView];
213
214    icon = [[DockIcon alloc] init];
215  }
216
217  return icon;
218}
219
220- (void)updateIcon {
221  DCHECK_CURRENTLY_ON(BrowserThread::UI);
222  static base::TimeDelta updateFrequency =
223      base::TimeDelta::FromMilliseconds(kUpdateFrequencyMs);
224
225  base::TimeTicks now = base::TimeTicks::Now();
226  base::TimeDelta timeSinceLastUpdate = now - lastUpdate_;
227  if (!forceUpdate_ && timeSinceLastUpdate < updateFrequency)
228    return;
229
230  lastUpdate_ = now;
231  forceUpdate_ = NO;
232
233  NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
234
235  [dockTile display];
236}
237
238- (void)setDownloads:(int)downloads {
239  DCHECK_CURRENTLY_ON(BrowserThread::UI);
240  NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
241  DockTileView* dockTileView = (DockTileView*)([dockTile contentView]);
242
243  if (downloads != [dockTileView downloads]) {
244    [dockTileView setDownloads:downloads];
245    forceUpdate_ = YES;
246  }
247}
248
249- (void)setIndeterminate:(BOOL)indeterminate {
250  DCHECK_CURRENTLY_ON(BrowserThread::UI);
251  NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
252  DockTileView* dockTileView = (DockTileView*)([dockTile contentView]);
253
254  if (indeterminate != [dockTileView indeterminate]) {
255    [dockTileView setIndeterminate:indeterminate];
256    forceUpdate_ = YES;
257  }
258}
259
260- (void)setProgress:(float)progress {
261  DCHECK_CURRENTLY_ON(BrowserThread::UI);
262  NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
263  DockTileView* dockTileView = (DockTileView*)([dockTile contentView]);
264
265  [dockTileView setProgress:progress];
266}
267
268@end
269