mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
Ensure data mutations from background readers are dispatched to the main thread to prevent conflicts with drawing logic (which runs on main thread). This resolves SIGTRAP and SIGABRT crashes observed during extended runtime.
330 lines
12 KiB
Swift
330 lines
12 KiB
Swift
//
|
|
// 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..<value.count {
|
|
var y = offset
|
|
for a in 0..<value[i].count {
|
|
let partitionValue = value[i][a]
|
|
let partitionHeight = maxPartitionHeight * CGFloat(partitionValue.value)
|
|
let partition = NSBezierPath(rect: NSRect(x: x, y: y, width: partitionWidth, height: partitionHeight))
|
|
|
|
if partitionValue.color == nil {
|
|
switch self.colorState {
|
|
case .systemAccent: NSColor.controlAccentColor.set()
|
|
case .utilization: partitionValue.value.usageColor(zones: colorZones, reversed: self.title == "Battery").set()
|
|
case .pressure: pressureLevel.pressureColor().set()
|
|
case .monochrome:
|
|
if self.boxState {
|
|
(isDarkMode ? NSColor.black : NSColor.white).set()
|
|
} else {
|
|
(isDarkMode ? NSColor.white : NSColor.black).set()
|
|
}
|
|
default: (self.colorState.additional as? NSColor ?? .controlAccentColor).set()
|
|
}
|
|
} else {
|
|
partitionValue.color?.set()
|
|
}
|
|
|
|
partition.fill()
|
|
partition.close()
|
|
|
|
y += partitionHeight
|
|
}
|
|
|
|
x += partitionWidth + partitionMargin
|
|
}
|
|
|
|
if self.boxState || self.frameState {
|
|
(isDarkMode ? NSColor.white : NSColor.black).set()
|
|
box.lineWidth = lineWidth
|
|
box.stroke()
|
|
}
|
|
|
|
self.setWidth(width)
|
|
}
|
|
|
|
public func setValue(_ newValue: [[ColorValue]]) {
|
|
DispatchQueue.main.async(execute: {
|
|
let tolerance: CGFloat = 0.01
|
|
let isDifferent = self._value.count != newValue.count || zip(self._value, newValue).contains { row1, row2 in
|
|
row1.count != row2.count || zip(row1, row2).contains { val1, val2 in
|
|
abs(val1.value - val2.value) > 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()
|
|
}
|
|
}
|