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.
388 lines
14 KiB
Swift
388 lines
14 KiB
Swift
//
|
|
// Chart.swift
|
|
// Kit
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 18/04/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
|
|
public class LineChart: WidgetWrapper {
|
|
private var labelState: Bool = false
|
|
private var boxState: Bool = true
|
|
private var frameState: Bool = false
|
|
private var valueState: Bool = false
|
|
private var valueColorState: Bool = false
|
|
private var colorState: SColor = .systemAccent
|
|
private var historyCount: Int = 60
|
|
private var scaleState: Scale = .none
|
|
|
|
private var chart: LineChartView = LineChartView(frame: NSRect(
|
|
x: 0,
|
|
y: 0,
|
|
width: 32,
|
|
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
|
|
), num: 60)
|
|
private var colors: [SColor] = SColor.allCases.filter({ $0 != SColor.cluster })
|
|
private var _value: Double = 0
|
|
private var _pressureLevel: RAMPressure = .normal
|
|
|
|
private var historyNumbers: [KeyValue_p] = [
|
|
KeyValue_t(key: "30", value: "30"),
|
|
KeyValue_t(key: "60", value: "60"),
|
|
KeyValue_t(key: "90", value: "90"),
|
|
KeyValue_t(key: "120", value: "120")
|
|
]
|
|
private var width: CGFloat {
|
|
get {
|
|
switch self.historyCount {
|
|
case 30:
|
|
return 24
|
|
case 60:
|
|
return 32
|
|
case 90:
|
|
return 42
|
|
case 120:
|
|
return 52
|
|
default:
|
|
return 32
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if let titleFromConfig = config!["Title"] as? String {
|
|
widgetTitle = titleFromConfig
|
|
}
|
|
if let label = config!["Label"] as? Bool {
|
|
self.labelState = label
|
|
}
|
|
if let box = config!["Box"] as? Bool {
|
|
self.boxState = box
|
|
}
|
|
if let value = config!["Value"] as? Bool {
|
|
self.valueState = value
|
|
}
|
|
if let unsupportedColors = config!["Unsupported colors"] as? [String] {
|
|
self.colors = self.colors.filter{ !unsupportedColors.contains($0.key) }
|
|
}
|
|
if let color = config!["Color"] as? String {
|
|
if let defaultColor = colors.first(where: { "\($0.self)" == color }) {
|
|
self.colorState = defaultColor
|
|
}
|
|
}
|
|
}
|
|
|
|
super.init(.lineChart, title: widgetTitle, frame: CGRect(
|
|
x: Constants.Widget.margin.x,
|
|
y: Constants.Widget.margin.y,
|
|
width: 32 + (Constants.Widget.margin.x*2),
|
|
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.valueState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_value", defaultValue: self.valueState)
|
|
self.labelState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
|
|
self.valueColorState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_valueColor", defaultValue: self.valueColorState)
|
|
self.colorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState.key))
|
|
self.historyCount = Store.shared.int(key: "\(self.title)_\(self.type.rawValue)_historyCount", defaultValue: self.historyCount)
|
|
self.scaleState = Scale.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_scale", defaultValue: self.scaleState.key))
|
|
|
|
self.chart.setScale(self.scaleState)
|
|
self.chart.reinit(self.historyCount)
|
|
}
|
|
|
|
if self.labelState {
|
|
self.setFrameSize(NSSize(width: Constants.Widget.width + 6 + (Constants.Widget.margin.x*2), height: self.frame.size.height))
|
|
}
|
|
|
|
if preview {
|
|
var list: [DoubleValue] = []
|
|
for _ in 0..<16 {
|
|
list.append(DoubleValue(Double.random(in: 0..<1)))
|
|
}
|
|
self.chart.points = list
|
|
self._value = 0.38
|
|
}
|
|
|
|
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)
|
|
|
|
guard let context = NSGraphicsContext.current?.cgContext else { return }
|
|
|
|
var value: Double = 0
|
|
var pressureLevel: RAMPressure = .normal
|
|
self.queue.sync {
|
|
value = self._value
|
|
pressureLevel = self._pressureLevel
|
|
}
|
|
|
|
var width = self.width + (Constants.Widget.margin.x*2)
|
|
var x: CGFloat = 0
|
|
let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1)
|
|
let offset = lineWidth / 2
|
|
var boxSize: CGSize = CGSize(width: self.width - (Constants.Widget.margin.x*2), height: self.frame.size.height)
|
|
|
|
var color: NSColor = .controlAccentColor
|
|
switch self.colorState {
|
|
case .systemAccent: color = .controlAccentColor
|
|
case .utilization: color = value.usageColor()
|
|
case .pressure: color = pressureLevel.pressureColor()
|
|
case .monochrome:
|
|
if self.boxState {
|
|
color = (isDarkMode ? NSColor.black : NSColor.white)
|
|
} else {
|
|
color = (isDarkMode ? NSColor.white : NSColor.black)
|
|
}
|
|
default: color = self.colorState.additional as? NSColor ?? .controlAccentColor
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if self.valueState {
|
|
let style = NSMutableParagraphStyle()
|
|
style.alignment = .right
|
|
|
|
var valueColor = isDarkMode ? NSColor.white : NSColor.black
|
|
if self.valueColorState {
|
|
valueColor = color
|
|
}
|
|
|
|
let stringAttributes = [
|
|
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 8, weight: .regular),
|
|
NSAttributedString.Key.foregroundColor: valueColor,
|
|
NSAttributedString.Key.paragraphStyle: style
|
|
]
|
|
|
|
let rect = CGRect(x: x+2, y: boxSize.height-7, width: boxSize.width - 2, height: 7)
|
|
let str = NSAttributedString.init(string: "\(Int((value.rounded(toPlaces: 2)) * 100))%", attributes: stringAttributes)
|
|
str.draw(with: rect)
|
|
|
|
boxSize.height = offset == 0.5 ? 10 : 9
|
|
}
|
|
|
|
let box = NSBezierPath(roundedRect: NSRect(
|
|
x: x+offset,
|
|
y: offset,
|
|
width: self.width - offset*2,
|
|
height: boxSize.height - (offset*2)
|
|
), xRadius: 2, yRadius: 2)
|
|
|
|
if self.boxState {
|
|
(isDarkMode ? NSColor.white : NSColor.black).set()
|
|
box.stroke()
|
|
box.fill()
|
|
self.chart.transparent = false
|
|
} else if self.frameState {
|
|
self.chart.transparent = true
|
|
} else {
|
|
self.chart.transparent = true
|
|
}
|
|
|
|
context.saveGState()
|
|
|
|
let chartFrame = NSRect(
|
|
x: x+offset+lineWidth,
|
|
y: offset,
|
|
width: box.bounds.width - (offset*2+lineWidth),
|
|
height: box.bounds.height - offset
|
|
)
|
|
self.chart.color = color
|
|
self.chart.setFrameSize(NSSize(width: chartFrame.width, height: chartFrame.height))
|
|
self.chart.draw(chartFrame)
|
|
|
|
context.restoreGState()
|
|
|
|
if self.boxState || self.frameState {
|
|
(isDarkMode ? NSColor.white : NSColor.black).set()
|
|
box.lineWidth = lineWidth
|
|
box.stroke()
|
|
}
|
|
|
|
self.setWidth(width)
|
|
}
|
|
|
|
public func setValue(_ newValue: Double) {
|
|
DispatchQueue.main.async(execute: {
|
|
self._value = newValue
|
|
self.chart.addValue(newValue)
|
|
self.display()
|
|
})
|
|
}
|
|
|
|
public func setPressure(_ newPressureLevel: RAMPressure) {
|
|
DispatchQueue.main.async(execute: {
|
|
guard self._pressureLevel != newPressureLevel else { return }
|
|
self._pressureLevel = newPressureLevel
|
|
self.display()
|
|
})
|
|
}
|
|
|
|
// 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("Value"), component: switchView(
|
|
action: #selector(self.toggleValue),
|
|
state: self.valueState
|
|
)),
|
|
PreferencesRow(localizedString("Box"), component: box),
|
|
PreferencesRow(localizedString("Frame"), component: frame),
|
|
PreferencesRow(localizedString("Color"), component: selectView(
|
|
action: #selector(self.toggleColor),
|
|
items: self.colors,
|
|
selected: self.colorState.key
|
|
)),
|
|
PreferencesRow(localizedString("Colorize value"), component: switchView(
|
|
action: #selector(self.toggleValueColor),
|
|
state: self.valueColorState
|
|
)),
|
|
PreferencesRow(localizedString("Number of reads in the chart"), component: selectView(
|
|
action: #selector(self.toggleHistoryCount),
|
|
items: self.historyNumbers,
|
|
selected: "\(self.historyCount)"
|
|
)),
|
|
PreferencesRow(localizedString("Scaling"), component: selectView(
|
|
action: #selector(self.toggleScale),
|
|
items: Scale.allCases.filter({ $0 != .fixed }),
|
|
selected: self.scaleState.key
|
|
))
|
|
]))
|
|
|
|
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.display()
|
|
}
|
|
|
|
@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.display()
|
|
}
|
|
|
|
@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.display()
|
|
}
|
|
|
|
@objc private func toggleValue(_ sender: NSControl) {
|
|
self.valueState = controlState(sender)
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_value", value: self.valueState)
|
|
self.display()
|
|
}
|
|
|
|
@objc private func toggleColor(_ sender: NSMenuItem) {
|
|
guard let key = sender.representedObject as? String else { return }
|
|
if let newColor = SColor.allCases.first(where: { $0.key == key }) {
|
|
self.colorState = newColor
|
|
}
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_color", value: key)
|
|
self.display()
|
|
}
|
|
|
|
@objc private func toggleValueColor(_ sender: NSControl) {
|
|
self.valueColorState = controlState(sender)
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_valueColor", value: self.valueColorState)
|
|
self.display()
|
|
}
|
|
|
|
@objc private func toggleHistoryCount(_ sender: NSMenuItem) {
|
|
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
|
|
self.historyCount = value
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_historyCount", value: value)
|
|
self.chart.reinit(value)
|
|
self.display()
|
|
}
|
|
|
|
@objc private func toggleScale(_ sender: NSMenuItem) {
|
|
guard let key = sender.representedObject as? String,
|
|
let value = Scale.allCases.first(where: { $0.key == key }) else { return }
|
|
self.scaleState = value
|
|
self.chart.setScale(value)
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_scale", value: key)
|
|
self.display()
|
|
}
|
|
}
|