| // 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/dock_icon.h" |
| |
| #include <stdint.h> |
| |
| #include "base/logging.h" |
| #include "base/mac/bundle_locations.h" |
| #include "base/mac/scoped_nsobject.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" |
| |
| using content::BrowserThread; |
| |
| namespace { |
| |
| // The fraction of the size of the dock icon that the badge is. |
| constexpr CGFloat kBadgeFraction = 0.375f; |
| constexpr CGFloat kBadgeMargin = 4; |
| constexpr CGFloat kBadgeStrokeWidth = 6; |
| |
| constexpr struct { |
| CGFloat offset, radius, opacity; |
| } kBadgeShadows[] = { |
| {0, 2, 0.14}, |
| {2, 2, 0.12}, |
| {1, 3, 0.2}, |
| }; |
| |
| // The maximum update rate for the dock icon. 200ms = 5fps. |
| constexpr int64_t kUpdateFrequencyMs = 200; |
| |
| } // namespace |
| |
| // A view that draws our dock tile. |
| @interface DockTileView : NSView { |
| @private |
| int downloads_; |
| BOOL indeterminate_; |
| float progress_; |
| } |
| |
| // Indicates how many downloads are in progress. |
| @property (nonatomic) int downloads; |
| |
| // Indicates whether the progress indicator should be in an indeterminate state |
| // or not. |
| @property (nonatomic) BOOL indeterminate; |
| |
| // Indicates the amount of progress made of the download. Ranges from [0..1]. |
| @property (nonatomic) float progress; |
| |
| @end |
| |
| @implementation DockTileView |
| |
| @synthesize downloads = downloads_; |
| @synthesize indeterminate = indeterminate_; |
| @synthesize progress = progress_; |
| |
| - (void)drawRect:(NSRect)dirtyRect { |
| // Not -[NSApplication applicationIconImage]; that fails to return a pasted |
| // custom icon. |
| NSString* appPath = [base::mac::MainBundle() bundlePath]; |
| NSImage* appIcon = [[NSWorkspace sharedWorkspace] iconForFile:appPath]; |
| [appIcon drawInRect:[self bounds] |
| fromRect:NSZeroRect |
| operation:NSCompositeSourceOver |
| fraction:1.0]; |
| |
| if (downloads_ == 0) |
| return; |
| |
| const CGFloat badgeSize = NSWidth(self.bounds) * kBadgeFraction; |
| const NSRect badgeRect = |
| NSMakeRect(NSMaxX(self.bounds) - badgeSize - kBadgeMargin, kBadgeMargin, |
| badgeSize, badgeSize); |
| const CGFloat badgeRadius = badgeSize / 2; |
| const NSPoint badgeCenter = NSMakePoint(NSMidX(badgeRect), NSMidY(badgeRect)); |
| |
| NSBezierPath* backgroundPath = |
| [NSBezierPath bezierPathWithOvalInRect:badgeRect]; |
| [[NSColor clearColor] setFill]; |
| |
| base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); |
| shadow.get().shadowColor = [NSColor blackColor]; |
| for (const auto shadowProps : kBadgeShadows) { |
| gfx::ScopedNSGraphicsContextSaveGState scopedGState; |
| shadow.get().shadowOffset = NSMakeSize(0, -shadowProps.offset); |
| shadow.get().shadowBlurRadius = shadowProps.radius; |
| [[NSColor colorWithCalibratedWhite:0 alpha:shadowProps.opacity] setFill]; |
| [shadow set]; |
| [backgroundPath fill]; |
| } |
| |
| [[NSColor colorWithCalibratedRed:0xec / 255.0 |
| green:0xf3 / 255.0 |
| blue:0xfe / 255.0 |
| alpha:1] setFill]; |
| [backgroundPath fill]; |
| |
| // Stroke |
| if (!indeterminate_) { |
| NSBezierPath* strokePath; |
| if (progress_ >= 1.0) { |
| strokePath = [NSBezierPath bezierPathWithOvalInRect:badgeRect]; |
| } else { |
| CGFloat endAngle = 90.0 - 360.0 * progress_; |
| if (endAngle < 0.0) |
| endAngle += 360.0; |
| strokePath = [NSBezierPath bezierPath]; |
| [strokePath |
| appendBezierPathWithArcWithCenter:badgeCenter |
| radius:badgeRadius - kBadgeStrokeWidth / 2 |
| startAngle:90.0 |
| endAngle:endAngle |
| clockwise:YES]; |
| } |
| [strokePath setLineWidth:kBadgeStrokeWidth]; |
| [[NSColor colorWithCalibratedRed:0x42 / 255.0 |
| green:0x85 / 255.0 |
| blue:0xf4 / 255.0 |
| alpha:1] setStroke]; |
| [strokePath stroke]; |
| } |
| |
| // Download count |
| base::scoped_nsobject<NSNumberFormatter> formatter( |
| [[NSNumberFormatter alloc] init]); |
| NSString* countString = |
| [formatter stringFromNumber:[NSNumber numberWithInt:downloads_]]; |
| |
| CGFloat countFontSize = 24; |
| NSSize countSize = NSZeroSize; |
| base::scoped_nsobject<NSAttributedString> countAttrString; |
| while (1) { |
| NSFont* countFont; |
| if (@available(macOS 10.11, *)) { |
| countFont = |
| [NSFont systemFontOfSize:countFontSize weight:NSFontWeightMedium]; |
| } else { |
| countFont = [[NSFontManager sharedFontManager] |
| convertWeight:YES |
| ofFont:[NSFont systemFontOfSize:countFontSize]]; |
| } |
| |
| // This will generally be plain Helvetica. |
| if (!countFont) |
| countFont = [NSFont userFontOfSize:countFontSize]; |
| |
| // Continued failure would generate an NSException. |
| if (!countFont) |
| break; |
| |
| countAttrString.reset([[NSAttributedString alloc] |
| initWithString:countString |
| attributes:@{ |
| NSForegroundColorAttributeName : |
| [NSColor colorWithCalibratedWhite:0 alpha:0.65], |
| NSFontAttributeName : countFont, |
| }]); |
| countSize = [countAttrString size]; |
| if (countSize.width > (badgeRadius - kBadgeStrokeWidth) * 1.5) { |
| countFontSize -= 1.0; |
| } else { |
| break; |
| } |
| } |
| |
| NSPoint countOrigin = badgeCenter; |
| countOrigin.x -= countSize.width / 2; |
| countOrigin.y -= countSize.height / 2; |
| |
| [countAttrString.get() drawAtPoint:countOrigin]; |
| } |
| |
| @end |
| |
| |
| @implementation DockIcon |
| |
| + (DockIcon*)sharedDockIcon { |
| static DockIcon* icon; |
| if (!icon) { |
| NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile]; |
| |
| base::scoped_nsobject<DockTileView> dockTileView( |
| [[DockTileView alloc] init]); |
| [dockTile setContentView:dockTileView]; |
| |
| icon = [[DockIcon alloc] init]; |
| } |
| |
| return icon; |
| } |
| |
| - (void)updateIcon { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| static base::TimeDelta updateFrequency = |
| base::TimeDelta::FromMilliseconds(kUpdateFrequencyMs); |
| |
| base::TimeTicks now = base::TimeTicks::Now(); |
| base::TimeDelta timeSinceLastUpdate = now - lastUpdate_; |
| if (!forceUpdate_ && timeSinceLastUpdate < updateFrequency) |
| return; |
| |
| lastUpdate_ = now; |
| forceUpdate_ = NO; |
| |
| NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile]; |
| |
| [dockTile display]; |
| } |
| |
| - (void)setDownloads:(int)downloads { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile]; |
| DockTileView* dockTileView = (DockTileView*)([dockTile contentView]); |
| |
| if (downloads != [dockTileView downloads]) { |
| [dockTileView setDownloads:downloads]; |
| forceUpdate_ = YES; |
| } |
| } |
| |
| - (void)setIndeterminate:(BOOL)indeterminate { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile]; |
| DockTileView* dockTileView = (DockTileView*)([dockTile contentView]); |
| |
| if (indeterminate != [dockTileView indeterminate]) { |
| [dockTileView setIndeterminate:indeterminate]; |
| forceUpdate_ = YES; |
| } |
| } |
| |
| - (void)setProgress:(float)progress { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile]; |
| DockTileView* dockTileView = (DockTileView*)([dockTile contentView]); |
| |
| [dockTileView setProgress:progress]; |
| } |
| |
| @end |