Skip to content

Commit

Permalink
Show progress in the Dock icon
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Feb 25, 2018
1 parent 3d61b42 commit dbb7784
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 4 deletions.
122 changes: 122 additions & 0 deletions Gifski/AppDelegate.swift
@@ -1,6 +1,125 @@
import Cocoa
import ProgressKit

/// TODO: Fade the progress-bar in on the first progress events
/// TODO: I will make this a separate SPM package soon
final class DockIconProgress {
private static let appIcon = NSApp.applicationIconImage!
private static var previousProgressValue: Double = 0
private static var progressObserver: NSKeyValueObservation?

private static var dockImageView: NSImageView = {
let dockImageView = NSImageView()
NSApp.dockTile.contentView = dockImageView
return dockImageView
}()

static var progress: Progress? {
didSet {
if let progress = progress {
progressObserver = progress.observe(\.fractionCompleted, options: .new) { object, _ in
progressValue = object.fractionCompleted
}
}
}
}

static var progressValue: Double = 0 {
didSet {
if previousProgressValue == 0 || (progressValue - previousProgressValue).magnitude > 0.001 {
previousProgressValue = progressValue
updateDockIcon()
}
}
}

enum ProgressStyle {
case bar
/// TODO: Make `color` optional when https://github.com/apple/swift-evolution/blob/master/proposals/0155-normalize-enum-case-representation.md is shipping in Swift
case circle(radius: Double, color: NSColor)
case custom(drawHandler: (_ rect: CGRect) -> Void)
}

static var style: ProgressStyle = .bar

/// TODO: Make the progress smoother by also animating the steps between each call to `updateDockIcon()`
private static func updateDockIcon() {
DispatchQueue.global(qos: .utility).async {
/// TODO: If the `progressValue` is 1, draw the full circle, then schedule another draw in n milliseconds to hide it
let icon = (0..<1).contains(self.progressValue) ? self.draw() : appIcon
DispatchQueue.main.async {
/// TODO: Make this better by drawing in the `contentView` directly instead of using an image
dockImageView.image = icon
NSApp.dockTile.display()
}
}
}

private static func draw() -> NSImage {
return NSImage(size: appIcon.size, flipped: false) { dstRect in
NSGraphicsContext.current?.imageInterpolation = .high
self.appIcon.draw(in: dstRect)

switch self.style {
case .bar:
self.drawProgressBar(dstRect)
case let .circle(radius, color):
self.drawProgressCircle(dstRect, radius: radius, color: color)
case let .custom(drawingHandler):
drawingHandler(dstRect)
}

return true
}
}

private static func drawProgressBar(_ dstRect: CGRect) {
func roundedRect(_ rect: CGRect) {
NSBezierPath(roundedRect: rect, cornerRadius: rect.height / 2).fill()
}

let bar = CGRect(x: 0, y: 20, width: dstRect.width, height: 10)
NSColor.white.withAlphaComponent(0.8).set()
roundedRect(bar)

let barInnerBg = bar.insetBy(dx: 0.5, dy: 0.5)
NSColor.black.withAlphaComponent(0.8).set()
roundedRect(barInnerBg)

var barProgress = bar.insetBy(dx: 1, dy: 1)
barProgress.size.width = barProgress.width * CGFloat(self.progressValue)
NSColor.white.set()
roundedRect(barProgress)
}

private static func drawProgressCircle(_ dstRect: CGRect, radius: Double, color: NSColor) {
guard let cgContext = NSGraphicsContext.current?.cgContext else {
return
}

let path = NSBezierPath()
let startAngle: CGFloat = 90
let endAngle = startAngle - (360 * CGFloat(self.progressValue))
path.appendArc(
withCenter: dstRect.center,
radius: CGFloat(radius),
startAngle: startAngle,
endAngle: endAngle,
clockwise: true
)

let arc = CAShapeLayer()
arc.path = path.cgPath
arc.lineCap = kCALineCapRound
arc.fillColor = nil
arc.strokeColor = color.cgColor
arc.lineWidth = 4
arc.cornerRadius = 3
arc.render(in: cgContext)
}
}


extension NSColor {
static let appTheme = NSColor(named: NSColor.Name("Theme"))!
}
Expand Down Expand Up @@ -189,5 +308,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
frameRate: choosenFrameRate
)
progress?.publish()

DockIconProgress.progress = progress
DockIconProgress.style = .circle(radius: 55, color: .appTheme)
}
}
6 changes: 2 additions & 4 deletions Gifski/Info.plist
Expand Up @@ -31,16 +31,14 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict/>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.photography</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>MDItemKeywords</key>
<string>gif,convert,video</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
Expand Down
30 changes: 30 additions & 0 deletions Gifski/util.swift
Expand Up @@ -55,6 +55,36 @@ class SSView: NSView {
}


extension NSBezierPath {
/// UIKit polyfill
var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)

for i in 0..<elementCount {
let type = element(at: i, associatedPoints: &points)
switch type {
case .moveToBezierPathElement:
path.move(to: points[0])
case .lineToBezierPathElement:
path.addLine(to: points[0])
case .curveToBezierPathElement:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePathBezierPathElement:
path.closeSubpath()
}
}

return path
}

/// UIKit polyfill
convenience init(roundedRect rect: CGRect, cornerRadius: CGFloat) {
self.init(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
}
}


extension AVAssetImageGenerator {
func generateCGImagesAsynchronously(forTimePoints timePoints: [CMTime], completionHandler: @escaping AVAssetImageGeneratorCompletionHandler) {
let times = timePoints.map { NSValue(time: $0) }
Expand Down

0 comments on commit dbb7784

Please sign in to comment.