// // BarChart.swift // Kit // // Created by Serhiy Mytrovtsiy on 26/04/2020. // Using Swift 5.0. // Running on macOS 10.15. // // Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved. // import Cocoa public class BarChart: WidgetWrapper { private var labelState: Bool = false private var boxState: Bool = true private var frameState: Bool = false public var colorState: SColor = .systemAccent private var colors: [SColor] = SColor.allCases private var _value: [[ColorValue]] = [[]] private var _pressureLevel: RAMPressure = .normal private var _colorZones: colorZones = (0.6, 0.8) private var boxSettingsView: NSSwitch? = nil private var frameSettingsView: NSSwitch? = nil public var NSLabelCharts: [NSAttributedString] = [] public init(title: String, config: NSDictionary?, preview: Bool = false) { var widgetTitle: String = title if config != nil { var configuration = config! if let titleFromConfig = config!["Title"] as? String { widgetTitle = titleFromConfig } if preview { if let previewConfig = config!["Preview"] as? NSDictionary { configuration = previewConfig if let value = configuration["Value"] as? String { self._value = value.split(separator: ",").map{ ([ColorValue(Double($0) ?? 0)]) } } } } if let label = configuration["Label"] as? Bool { self.labelState = label } if let box = configuration["Box"] as? Bool { self.boxState = box } if let unsupportedColors = configuration["Unsupported colors"] as? [String] { self.colors = self.colors.filter{ !unsupportedColors.contains($0.key) } } if let color = configuration["Color"] as? String { if let defaultColor = self.colors.first(where: { "\($0.self)" == color }) { self.colorState = defaultColor } } } super.init(.barChart, title: widgetTitle, frame: CGRect( x: Constants.Widget.margin.x, y: Constants.Widget.margin.y, width: Constants.Widget.width + (2*Constants.Widget.margin.x), height: Constants.Widget.height - (2*Constants.Widget.margin.y) )) self.canDrawConcurrently = true if !preview { self.boxState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_box", defaultValue: self.boxState) self.frameState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_frame", defaultValue: self.frameState) self.labelState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState) self.colorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState.key)) } if preview { if self._value.isEmpty { self._value = [[ColorValue(0.72)], [ColorValue(0.38)]] } self.setFrameSize(NSSize(width: 36, height: self.frame.size.height)) self.invalidateIntrinsicContentSize() self.display() } let style = NSMutableParagraphStyle() style.alignment = .center let stringAttributes = [ NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular), NSAttributedString.Key.foregroundColor: NSColor.textColor, NSAttributedString.Key.paragraphStyle: style ] for char in String(self.title.prefix(3)).uppercased().reversed() { let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes) self.NSLabelCharts.append(str) } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) var value: [[ColorValue]] = [] var pressureLevel: RAMPressure = .normal var colorZones: colorZones = (0.6, 0.8) self.queue.sync { value = self._value pressureLevel = self._pressureLevel colorZones = self._colorZones } guard !value.isEmpty else { self.setWidth(0) return } var width: CGFloat = Constants.Widget.margin.x*2 var x: CGFloat = 0 let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1) let offset = lineWidth / 2 switch value.count { case 0, 1: width += 10 + (offset*2) case 2: width += 22 case 3...4: // 3,4 width += 30 case 5...8: // 5,6,7,8 width += 40 case 9...12: // 9..12 width += 50 case 13...16: // 13..16 width += 76 case 17...32: // 17..32 width += 84 default: // > 32 width += 118 } if self.labelState { let letterHeight = self.frame.height / 3 let letterWidth: CGFloat = 6.0 var yMargin: CGFloat = 0 for char in self.NSLabelCharts { let rect = CGRect(x: x, y: yMargin, width: letterWidth, height: letterHeight) char.draw(with: rect) yMargin += letterHeight } width += letterWidth + Constants.Widget.spacing x = letterWidth + Constants.Widget.spacing } let box = NSBezierPath(roundedRect: NSRect( x: x + offset, y: offset, width: width - x - (offset*2) - (Constants.Widget.margin.x*2), height: self.frame.size.height - (offset*2) ), xRadius: 2, yRadius: 2) if self.boxState { (isDarkMode ? NSColor.white : NSColor.black).set() box.stroke() box.fill() } let widthForBarChart = box.bounds.width let partitionMargin: CGFloat = 0.5 let partitionsMargin: CGFloat = (CGFloat(value.count - 1)) * partitionMargin / CGFloat(value.count - 1) let partitionWidth: CGFloat = (widthForBarChart / CGFloat(value.count)) - CGFloat(partitionsMargin.isNaN ? 0 : partitionsMargin) let maxPartitionHeight: CGFloat = box.bounds.height x += offset for i in 0.. tolerance || val1.color != val2.color } } guard isDifferent else { return } self._value = newValue self.redraw() }) } public func setPressure(_ newPressureLevel: RAMPressure) { DispatchQueue.main.async(execute: { guard self._pressureLevel != newPressureLevel else { return } self._pressureLevel = newPressureLevel self.redraw() }) } public func setColorZones(_ newColorZones: colorZones) { DispatchQueue.main.async(execute: { guard self._colorZones != newColorZones else { return } self._colorZones = newColorZones self.redraw() }) } // MARK: - Settings public override func settings() -> NSView { let view = SettingsContainerView() let box = switchView( action: #selector(self.toggleBox), state: self.boxState ) self.boxSettingsView = box let frame = switchView( action: #selector(self.toggleFrame), state: self.frameState ) self.frameSettingsView = frame view.addArrangedSubview(PreferencesSection([ PreferencesRow(localizedString("Label"), component: switchView( action: #selector(self.toggleLabel), state: self.labelState )), PreferencesRow(localizedString("Color"), component: selectView( action: #selector(self.toggleColor), items: self.colors, selected: self.colorState.key )), PreferencesRow(localizedString("Box"), component: box), PreferencesRow(localizedString("Frame"), component: frame) ])) return view } @objc private func toggleLabel(_ sender: NSControl) { self.labelState = controlState(sender) Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState) self.redraw() } @objc private func toggleBox(_ sender: NSControl) { self.boxState = controlState(sender) Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState) if self.frameState { self.frameSettingsView?.state = .off self.frameState = false Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState) } self.redraw() } @objc private func toggleFrame(_ sender: NSControl) { self.frameState = controlState(sender) Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState) if self.boxState { self.boxSettingsView?.state = .off self.boxState = false Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState) } self.redraw() } @objc private func toggleColor(_ sender: NSMenuItem) { guard let key = sender.representedObject as? String else { return } if let newColor = self.colors.first(where: { $0.key == key }) { self.colorState = newColor } Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_color", value: key) self.redraw() } }