// // LinearProgressControl.swift // TGUIKit // // Created by keepcoder on 28/10/2016. // Copyright © 2016 Telegram. All rights reserved. // import Cocoa public enum LinearProgressAlignment { case bottom case center case top } public class LinearProgressControl: Control { public var hasMinumimVisibility: Bool = false private var progressView:View! private var fetchingView:View! private var fetchingViewRanges:[View] = [] private var containerView:View! private var _progress:CGFloat = 0 private var progress:CGFloat { get { return scrubblingTempState ?? _progress } set { _progress = newValue } } private var fetchingProgress: CGFloat = 0 private var fetchingProgressRanges: [Range] = [] public var progressHeight:CGFloat public var onUserChanged:((Float)->Void)? public var onLiveScrobbling:((Float?)->Void)? public var startScrobbling:(()->Void)? public var endScrobbling:(()->Void)? public var insets: NSEdgeInsets = NSEdgeInsets() { didSet { needsLayout = true } } public var alignment: LinearProgressAlignment = .bottom public var liveScrobbling: Bool = true private var scrubber: ImageButton? = nil private(set) public var scrubblingTempState: CGFloat? { didSet { needsLayout = true self.progressView.layer?.removeAllAnimations() self.scrubber?.layer?.removeAllAnimations() } } public var cornerRadius: CGFloat = 0 { didSet { self.progressView.layer?.cornerRadius = cornerRadius self.layer?.cornerRadius = cornerRadius } } public var scrubberImage: CGImage? { didSet { if let scrubberImage = scrubberImage { if scrubber == nil { scrubber = ImageButton() scrubber?.userInteractionEnabled = false scrubber?.autohighlight = false scrubber!.set(image: scrubberImage, for: .Normal) _ = scrubber!.sizeToFit() addSubview(scrubber!) } else { scrubber!.set(image: scrubberImage, for: .Normal) _ = scrubber!.sizeToFit() } needsLayout = true } else { scrubber?.removeFromSuperview() scrubber = nil } } } public var roundCorners: Bool = false { didSet { containerView.layer?.cornerRadius = roundCorners ? containerView.frame.height / 2 : 0 progressView.layer?.cornerRadius = roundCorners ? progressView.frame.height / 2 : 0 fetchingView.layer?.cornerRadius = roundCorners ? fetchingView.frame.height / 2 : 0 for view in fetchingViewRanges { view.layer?.cornerRadius = roundCorners ? view.frame.height / 2 : 0 } } } public var currentValue: CGFloat { return progress } public override func mouseDragged(with event: NSEvent) { super.mouseDragged(with: event) if let onUserChanged = onUserChanged, isEnabled { let location = containerView.convert(event.locationInWindow, from: nil) // if location.x >= 0 && location.x <= frame.width { let progress = min(max(Float(max(location.x, 0) / containerView.frame.width), 0), 1) if liveScrobbling { onUserChanged(progress) } else { self.scrubblingTempState = CGFloat(progress) self.onLiveScrobbling?(progress) } // } } } public override func mouseDown(with event: NSEvent) { scrubblingTempState = nil self.started = true self.startScrobbling?() if let _ = onUserChanged, !liveScrobbling, isEnabled { let location = containerView.convert(event.locationInWindow, from: nil) let progress = min(max(CGFloat(max(location.x, 0) / containerView.frame.width), 0), 1) self.scrubblingTempState = progress self.onLiveScrobbling?(nil) // } } else if !userInteractionEnabled { //super.mouseDown(with: event) } } private var started: Bool = false public var hasTemporaryState: Bool { return scrubblingTempState != nil || started } public override func mouseUp(with event: NSEvent) { scrubblingTempState = nil self.started = false self.endScrobbling?() if let onUserChanged = onUserChanged, isEnabled { let location = containerView.convert(event.locationInWindow, from: nil) let progress = min(max(Float(max(location.x, 0) / containerView.frame.width), 0), 1) onUserChanged(progress) self.onLiveScrobbling?(nil) } } public var interactiveValue:Float { if let window = window { let location = containerView.convert(window.mouseLocationOutsideOfEventStream, from: nil) return Float(location.x / containerView.frame.width) } return 0 } open override func updateTrackingAreas() { super.updateTrackingAreas(); if let trackingArea = trackingArea { self.removeTrackingArea(trackingArea) } trackingArea = nil if let _ = window { let options:NSTrackingArea.Options = [NSTrackingArea.Options.cursorUpdate, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.mouseMoved, NSTrackingArea.Options.enabledDuringMouseDrag, NSTrackingArea.Options.activeInKeyWindow,NSTrackingArea.Options.inVisibleRect] self.trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) self.addTrackingArea(self.trackingArea!) } } deinit { if let trackingArea = self.trackingArea { self.removeTrackingArea(trackingArea) } } public override var style: ControlStyle { set { self.progressView.layer?.backgroundColor = newValue.foregroundColor.cgColor super.style = newValue } get { return super.style } } public var containerBackground: NSColor = .clear { didSet { containerView.backgroundColor = containerBackground } } public var fetchingColor: NSColor = presentation.colors.grayTransparent { didSet { self.fetchingView.layer?.backgroundColor = fetchingColor.cgColor for fetchView in fetchingViewRanges { fetchView.backgroundColor = fetchingColor } } } private func preparedAnimation(keyPath: String, from: NSValue, to: NSValue, duration: Double, beginTime: Double?, offset: Double, speed: Float, repeatForever: Bool = false) -> CAAnimation { let animation = CABasicAnimation(keyPath: keyPath) animation.fromValue = from animation.toValue = to animation.duration = duration animation.fillMode = .both animation.speed = speed animation.timeOffset = offset animation.isAdditive = false if let beginTime = beginTime { animation.beginTime = beginTime } return animation } public func set(progress:CGFloat, animated:Bool, duration: Double, beginTime: Double?, offset: Double, speed: Float, repeatForever: Bool = false) { let progress:CGFloat = progress.isNaN ? 1 : progress self.progress = progress let size = NSMakeSize(floorToScreenPixels(backingScaleFactor, max(containerView.frame.width * self.progress, hasMinumimVisibility ? progressHeight : 0)), progressHeight) progressView.centerY(x: 0) let fromBounds = NSMakeRect(0, progressView.frame.minY, 0, size.height) let toBounds = NSMakeRect(0, progressView.frame.minY, containerView.frame.width, size.height) if animated, scrubblingTempState == nil { progressView.layer?.add(preparedAnimation(keyPath: "bounds", from: NSValue(rect: fromBounds), to: NSValue(rect: toBounds), duration: duration, beginTime: beginTime, offset: offset, speed: speed), forKey: "bounds") if let scrubber = scrubber { scrubber.layer?.add(preparedAnimation(keyPath: "position", from: NSValue(point: NSMakePoint(containerView.frame.minX - scrubber.frame.width / 2, scrubber.frame.minY)), to: NSValue(point: NSMakePoint(containerView.frame.maxX - scrubber.frame.width / 2, scrubber.frame.minY)), duration: duration, beginTime: beginTime, offset: offset, speed: speed), forKey: "position") } } else { progressView.layer?.removeAllAnimations() set(progress: progress) } updateFetchingRanges(animated) } public override var frame: NSRect { didSet { layout() } } public func set(progress:CGFloat, animated:Bool = false, duration: Double = 0.2, timingFunction: CAMediaTimingFunctionName = .linear, bounce: Bool = false) { let progress:CGFloat = progress.isNaN ? 1 : progress self.progress = progress let size = NSMakeSize(floorToScreenPixels(backingScaleFactor, max(containerView.frame.width * self.progress, hasMinumimVisibility ? progressHeight : 0)), progressHeight) if animated { let size = NSMakeSize(size.width - progressView.frame.width, size.height - progressView.frame.height) progressView.layer?.animateBounds(from: CGRect(origin: .zero, size: size), to: .zero, duration: duration, timingFunction: timingFunction, additive: true) } else { progressView.layer?.removeAnimation(forKey: "bounds") } progressView.change(size: size, animated: animated, duration: duration, timingFunction: timingFunction) if let scrubber = scrubber { scrubber.change(pos: NSMakePoint(max(0, min(containerView.frame.minX + size.width - scrubber.frame.width / 2, containerView.frame.maxX - scrubber.frame.width)), scrubber.frame.minY), animated: animated, timingFunction: timingFunction) } progressView.centerY(x: 0) updateFetchingRanges(animated) } public func set(fetchingProgress: CGFloat, animated:Bool = false, duration: Double = 0.2) { let fetchingProgress:CGFloat = fetchingProgress.isNaN ? 1 : fetchingProgress self.fetchingProgress = fetchingProgress let size = NSMakeSize(floorToScreenPixels(backingScaleFactor, containerView.frame.width * fetchingProgress), progressHeight) fetchingView.change(size: size, animated: animated, duration: duration, timingFunction: .linear) fetchingView.centerY(x: 0) } public func set(fetchingProgressRanges: [Range], animated: Bool = true) { self.fetchingProgressRanges = fetchingProgressRanges updateFetchingRanges(animated) } private func updateFetchingRanges(_ animated: Bool) { let fetchingProgressRanges = self.fetchingProgressRanges.filter({$0.contains(self.currentValue)}) if self.fetchingViewRanges.count == fetchingProgressRanges.count { } else if self.fetchingViewRanges.count > fetchingProgressRanges.count { while self.fetchingViewRanges.count != fetchingProgressRanges.count { self.fetchingViewRanges.removeLast().removeFromSuperview() } } else if self.fetchingViewRanges.count < fetchingProgressRanges.count { while self.fetchingViewRanges.count != fetchingProgressRanges.count { let view = View(frame: NSMakeRect(0, 0, 0, progressHeight)) view.backgroundColor = fetchingColor self.fetchingViewRanges.append(view) containerView.addSubview(self.fetchingViewRanges.last!, positioned: .below, relativeTo: progressView) } } for i in 0 ..< fetchingProgressRanges.count { let range = fetchingProgressRanges[i] let view = self.fetchingViewRanges[i] let width = (range.upperBound - range.lowerBound) * containerView.frame.width view.change(size: NSMakeSize(width, progressHeight), animated: animated, duration: 0.2, timingFunction: .linear) view.setFrameOrigin(range.lowerBound * containerView.frame.width, floorToScreenPixels(backingScaleFactor, (containerView.frame.height - progressHeight) / 2)) view.layer?.cornerRadius = roundCorners ? view.frame.height / 2 : 0 } } public init(progressHeight:CGFloat = 4) { self.progressHeight = progressHeight super.init(frame: NSMakeRect(0, 0, 0, progressHeight)) initialize() } public override func layout() { super.layout() switch alignment { case .bottom: containerView.frame = NSMakeRect(insets.left, frame.height - progressHeight, frame.width - insets.left - insets.right, progressHeight) case .top: containerView.frame = NSMakeRect(insets.left, 0, frame.width - insets.left - insets.right, progressHeight) case .center: containerView.frame = NSMakeRect(insets.left, floorToScreenPixels(backingScaleFactor, (frame.height - progressHeight) / 2), frame.width - insets.left - insets.right, progressHeight) } let size = NSMakeSize(floorToScreenPixels(backingScaleFactor, max(containerView.frame.width * self.progress, hasMinumimVisibility ? progressHeight : 0)), progressHeight) progressView.setFrameSize(size) if let scrubber = scrubber { scrubber.centerY(x: max(0, min(containerView.frame.minX + size.width - scrubber.frame.width / 2, containerView.frame.maxX - scrubber.frame.width))) } } private func initialize() { containerView = View(frame:NSMakeRect(0, 0, 0, progressHeight)) addSubview(containerView) fetchingView = View(frame:NSMakeRect(0, 0, 0, progressHeight)) fetchingView.backgroundColor = style.foregroundColor containerView.addSubview(fetchingView) progressView = View(frame:NSMakeRect(0, 0, 0, progressHeight)) progressView.backgroundColor = style.foregroundColor containerView.addSubview(progressView) userInteractionEnabled = false progressView.isEventLess = true containerView.isEventLess = true fetchingView.isEventLess = true } public var highlightOnHover: Bool = true private func updateCursor() { if mouseInside() && onUserChanged != nil, style.highlightColor != .clear, isEnabled { if highlightOnHover { set(background: style.highlightColor.withAlphaComponent(0.2), for: .Hover) } } else { set(background: style.backgroundColor, for: .Hover) } } public override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) updateCursor() } public override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) updateCursor() } public override func mouseMoved(with event: NSEvent) { super.mouseMoved(with: event) updateCursor() } required public init(frame frameRect: NSRect) { self.progressHeight = frameRect.height super.init(frame:frameRect) initialize() } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }