mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
457 lines
18 KiB
Swift
457 lines
18 KiB
Swift
//
|
|
// popup.swift
|
|
// GPU
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 17/08/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import Kit
|
|
|
|
internal class Popup: PopupWrapper {
|
|
public init() {
|
|
super.init(ModuleType.GPU, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
|
|
|
|
self.orientation = .vertical
|
|
self.spacing = Constants.Popup.margins
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
internal func infoCallback(_ value: GPUs) {
|
|
if self.arrangedSubviews.filter({ $0 is GPUView }).count != value.list.count {
|
|
self.arrangedSubviews.forEach{ $0.removeFromSuperview() }
|
|
}
|
|
|
|
value.list.reversed().forEach { (gpu: GPU_Info) in
|
|
if let view = self.arrangedSubviews.filter({ $0 is GPUView }).map({ $0 as! GPUView }).first(where: { $0.value.id == gpu.id }) {
|
|
view.update(gpu)
|
|
} else {
|
|
self.addArrangedSubview(GPUView(
|
|
width: self.frame.width,
|
|
gpu: gpu,
|
|
callback: self.recalculateHeight
|
|
))
|
|
}
|
|
}
|
|
|
|
self.recalculateHeight()
|
|
}
|
|
|
|
private func recalculateHeight() {
|
|
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
|
|
if self.frame.size.height != h && h >= 0 {
|
|
self.setFrameSize(NSSize(width: self.frame.width, height: h))
|
|
self.sizeCallback?(self.frame.size)
|
|
}
|
|
}
|
|
|
|
// MARK: - Settings
|
|
|
|
public override func settings() -> NSView? {
|
|
let view = SettingsContainerView()
|
|
|
|
view.addArrangedSubview(PreferencesSection([
|
|
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
|
|
callback: self.setKeyboardShortcut,
|
|
value: self.keyboardShortcut
|
|
))
|
|
]))
|
|
|
|
return view
|
|
}
|
|
}
|
|
|
|
private class GPUView: NSStackView {
|
|
public var value: GPU_Info
|
|
private var detailsView: GPUDetails
|
|
private let circleSize: CGFloat = 50
|
|
private let chartSize: CGFloat = 60
|
|
|
|
private var stateView: NSView? = nil
|
|
private var circleRow: NSStackView? = nil
|
|
private var chartRow: NSStackView? = nil
|
|
|
|
private var temperatureChart: LineChartView? = nil
|
|
private var utilizationChart: LineChartView? = nil
|
|
private var renderUtilizationChart: LineChartView? = nil
|
|
private var tilerUtilizationChart: LineChartView? = nil
|
|
|
|
public var sizeCallback: (() -> Void)
|
|
|
|
open override var intrinsicContentSize: CGSize {
|
|
return CGSize(width: self.bounds.width, height: self.bounds.height)
|
|
}
|
|
|
|
public init(width: CGFloat, gpu: GPU_Info, callback: @escaping (() -> Void)) {
|
|
self.value = gpu
|
|
self.detailsView = GPUDetails(width: width, value: gpu)
|
|
self.sizeCallback = callback
|
|
|
|
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 0))
|
|
|
|
self.orientation = .vertical
|
|
self.alignment = .centerX
|
|
self.distribution = .fillProportionally
|
|
self.spacing = 0
|
|
self.wantsLayer = true
|
|
self.layer?.cornerRadius = 2
|
|
|
|
self.addArrangedSubview(self.title())
|
|
self.addArrangedSubview(self.stats())
|
|
self.addArrangedSubview(NSView())
|
|
|
|
let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
|
|
self.setFrameSize(NSSize(width: self.frame.width, height: h))
|
|
self.sizeCallback()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func updateLayer() {
|
|
self.layer?.backgroundColor = (isDarkMode ? NSColor(red: 17/255, green: 17/255, blue: 17/255, alpha: 0.25) : NSColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)).cgColor
|
|
}
|
|
|
|
private func title() -> NSView {
|
|
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 24))
|
|
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
|
|
|
|
let width: CGFloat = self.value.model.widthOfString(usingFont: NSFont.systemFont(ofSize: 13, weight: .regular)) + 16
|
|
let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: (view.frame.height-16)/2, width: width - 8, height: 16))
|
|
labelView.alignment = .center
|
|
labelView.textColor = .secondaryLabelColor
|
|
labelView.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
|
labelView.stringValue = self.value.model
|
|
|
|
let stateView: NSView = NSView(frame: NSRect(x: width - 8, y: (view.frame.height-7)/2, width: 6, height: 6))
|
|
stateView.wantsLayer = true
|
|
stateView.layer?.backgroundColor = (self.value.state ? NSColor.systemGreen : NSColor.systemRed).cgColor
|
|
stateView.toolTip = localizedString("GPU \(self.value.state ? "enabled" : "disabled")")
|
|
stateView.layer?.cornerRadius = 4
|
|
|
|
let details = localizedString("Details").uppercased()
|
|
let w = details.widthOfString(usingFont: NSFont.systemFont(ofSize: 9, weight: .regular)) + 8
|
|
let button = NSButtonWithPadding()
|
|
button.frame = CGRect(x: view.frame.width - w, y: 2, width: w, height: view.frame.height-2)
|
|
button.verticalPadding = 9
|
|
button.bezelStyle = .regularSquare
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.isBordered = false
|
|
button.action = #selector(self.showDetails)
|
|
button.target = self
|
|
button.toolTip = localizedString("Details")
|
|
button.title = details
|
|
button.font = NSFont.systemFont(ofSize: 9, weight: .regular)
|
|
|
|
view.addSubview(labelView)
|
|
view.addSubview(stateView)
|
|
view.addSubview(button)
|
|
self.stateView = stateView
|
|
|
|
return view
|
|
}
|
|
|
|
private func stats() -> NSView {
|
|
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
|
|
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
|
|
container.orientation = .vertical
|
|
container.spacing = 0
|
|
|
|
let circles: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
|
|
circles.orientation = .horizontal
|
|
circles.distribution = .fillEqually
|
|
circles.alignment = .bottom
|
|
self.circleRow = circles
|
|
|
|
let charts: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
|
|
charts.orientation = .horizontal
|
|
charts.distribution = .fillEqually
|
|
self.chartRow = charts
|
|
|
|
self.addStats(id: "GPU temperature", self.value.temperature)
|
|
self.addStats(id: "GPU utilization", self.value.utilization)
|
|
self.addStats(id: "Render utilization", self.value.renderUtilization)
|
|
self.addStats(id: "Tiler utilization", self.value.tilerUtilization)
|
|
|
|
container.addArrangedSubview(circles)
|
|
container.addArrangedSubview(charts)
|
|
|
|
view.addSubview(container)
|
|
|
|
let h = container.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
|
|
view.setFrameSize(NSSize(width: self.frame.width, height: h))
|
|
container.setFrameSize(NSSize(width: self.frame.width, height: view.bounds.height))
|
|
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
|
|
container.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
|
|
|
|
return view
|
|
}
|
|
|
|
private func addStats(id: String, _ val: Double? = nil) {
|
|
guard let value = val else { return }
|
|
|
|
var circle: HalfCircleGraphView
|
|
var chart: LineChartView
|
|
|
|
if let view = self.circleRow?.arrangedSubviews.filter({ $0 is HalfCircleGraphView }).first(where: { ($0 as! HalfCircleGraphView).id == id }) {
|
|
circle = view as! HalfCircleGraphView
|
|
} else {
|
|
circle = HalfCircleGraphView(frame: NSRect(x: 0, y: 0, width: circleSize, height: circleSize))
|
|
circle.id = id
|
|
circle.toolTip = localizedString(id)
|
|
if let row = self.circleRow {
|
|
row.setFrameSize(NSSize(width: row.frame.width, height: self.circleSize + 20))
|
|
row.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 0, right: 10)
|
|
row.heightAnchor.constraint(equalToConstant: row.bounds.height).isActive = true
|
|
row.addArrangedSubview(circle)
|
|
}
|
|
}
|
|
|
|
if let view = self.chartRow?.arrangedSubviews.filter({ $0 is LineChartView }).first(where: { ($0 as! LineChartView).id == id }) {
|
|
chart = view as! LineChartView
|
|
} else {
|
|
chart = LineChartView(frame: NSRect(x: 0, y: 0, width: 100, height: self.chartSize), num: 120)
|
|
chart.isTooltipEnabled = false
|
|
chart.wantsLayer = true
|
|
chart.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
|
|
chart.layer?.cornerRadius = 3
|
|
chart.id = id
|
|
chart.toolTip = localizedString(id)
|
|
if let row = self.chartRow {
|
|
row.setFrameSize(NSSize(width: row.frame.width, height: self.chartSize + 20))
|
|
row.spacing = Constants.Popup.margins
|
|
row.edgeInsets = NSEdgeInsets(
|
|
top: Constants.Popup.margins,
|
|
left: Constants.Popup.margins,
|
|
bottom: Constants.Popup.margins,
|
|
right: Constants.Popup.margins
|
|
)
|
|
row.heightAnchor.constraint(equalToConstant: row.bounds.height).isActive = true
|
|
row.addArrangedSubview(chart)
|
|
}
|
|
}
|
|
|
|
if id == "GPU temperature" {
|
|
circle.setValue(value)
|
|
circle.setText(temperature(value))
|
|
circle.toolTip = "\(localizedString(id)): \(temperature(value))"
|
|
chart.suffix = UnitTemperature.current.symbol
|
|
|
|
if self.temperatureChart == nil {
|
|
self.temperatureChart = chart
|
|
}
|
|
} else if id == "GPU utilization" {
|
|
circle.setValue(value)
|
|
circle.setText("\(Int(value*100))%")
|
|
circle.toolTip = "\(localizedString(id)): \(Int(value*100))%"
|
|
|
|
if self.utilizationChart == nil {
|
|
self.utilizationChart = chart
|
|
}
|
|
} else if id == "Render utilization" {
|
|
circle.setValue(value)
|
|
circle.setText("\(Int(value*100))%")
|
|
circle.toolTip = "\(localizedString(id)): \(Int(value*100))%"
|
|
|
|
if self.renderUtilizationChart == nil {
|
|
self.renderUtilizationChart = chart
|
|
}
|
|
} else if id == "Tiler utilization" {
|
|
circle.setValue(value)
|
|
circle.setText("\(Int(value*100))%")
|
|
circle.toolTip = "\(localizedString(id)): \(Int(value*100))%"
|
|
|
|
if self.tilerUtilizationChart == nil {
|
|
self.tilerUtilizationChart = chart
|
|
}
|
|
}
|
|
}
|
|
|
|
public func update(_ gpu: GPU_Info) {
|
|
self.detailsView.update(gpu)
|
|
|
|
if self.window?.isVisible ?? false {
|
|
self.stateView?.layer?.backgroundColor = (gpu.state ? NSColor.systemGreen : NSColor.systemRed).cgColor
|
|
self.stateView?.toolTip = localizedString("GPU \(gpu.state ? "enabled" : "disabled")")
|
|
|
|
self.addStats(id: "GPU temperature", gpu.temperature)
|
|
self.addStats(id: "GPU utilization", gpu.utilization)
|
|
self.addStats(id: "Render utilization", gpu.renderUtilization)
|
|
self.addStats(id: "Tiler utilization", gpu.tilerUtilization)
|
|
}
|
|
|
|
if let value = gpu.temperature {
|
|
if let temp = Double(temperature(value).replacingOccurrences(of: "C", with: "").replacingOccurrences(of: "F", with: "").digits) {
|
|
self.temperatureChart?.addValue(temp/100)
|
|
} else {
|
|
self.temperatureChart?.addValue(value)
|
|
}
|
|
}
|
|
if let value = gpu.utilization {
|
|
self.utilizationChart?.addValue(value)
|
|
}
|
|
if let value = gpu.renderUtilization {
|
|
self.renderUtilizationChart?.addValue(value)
|
|
}
|
|
if let value = gpu.tilerUtilization {
|
|
self.tilerUtilizationChart?.addValue(value)
|
|
}
|
|
}
|
|
|
|
@objc private func showDetails() {
|
|
if let view = self.arrangedSubviews.first(where: { $0 is GPUDetails }) {
|
|
view.removeFromSuperview()
|
|
} else {
|
|
self.insertArrangedSubview(self.detailsView, at: 1)
|
|
}
|
|
|
|
self.setFrameSize(NSSize(
|
|
width: self.frame.width,
|
|
height: self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +)
|
|
))
|
|
self.sizeCallback()
|
|
}
|
|
}
|
|
|
|
private class GPUDetails: NSView {
|
|
private var status: NSTextField? = nil
|
|
private var fanSpeed: NSTextField? = nil
|
|
private var coreClock: NSTextField? = nil
|
|
private var memoryClock: NSTextField? = nil
|
|
private var temperature: NSTextField? = nil
|
|
private var utilization: NSTextField? = nil
|
|
private var renderUtilization: NSTextField? = nil
|
|
private var tilerUtilization: NSTextField? = nil
|
|
|
|
open override var intrinsicContentSize: CGSize {
|
|
return CGSize(width: self.bounds.width, height: self.bounds.height)
|
|
}
|
|
|
|
init(width: CGFloat, value: GPU_Info) {
|
|
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 0))
|
|
|
|
let grid: NSGridView = NSGridView(frame: NSRect(
|
|
x: Constants.Popup.margins, y: Constants.Popup.margins,
|
|
width: self.frame.width - (Constants.Popup.margins*2), height: 0
|
|
))
|
|
grid.yPlacement = .center
|
|
grid.xPlacement = .leading
|
|
grid.rowSpacing = 0
|
|
grid.columnSpacing = 0
|
|
|
|
var num: CGFloat = 2
|
|
|
|
if let value = value.vendor {
|
|
grid.addRow(with: keyValueRow("\(localizedString("Vendor")):", value))
|
|
num += 1
|
|
}
|
|
|
|
grid.addRow(with: keyValueRow("\(localizedString("Model")):", value.model))
|
|
|
|
if let value = value.cores {
|
|
let arr = keyValueRow("\(localizedString("Cores")):", "\(value)")
|
|
grid.addRow(with: arr)
|
|
num += 1
|
|
}
|
|
|
|
let state: String = value.state ? localizedString("Active") : localizedString("Non active")
|
|
let arr = keyValueRow("\(localizedString("Status")):", state)
|
|
self.status = arr.last
|
|
grid.addRow(with: arr)
|
|
|
|
if let value = value.fanSpeed {
|
|
let arr = keyValueRow("\(localizedString("Fan speed")):", "\(value)%")
|
|
self.fanSpeed = arr.last
|
|
grid.addRow(with: arr)
|
|
num += 1
|
|
}
|
|
if let value = value.coreClock {
|
|
let arr = keyValueRow("\(localizedString("Core clock")):", "\(value)MHz")
|
|
self.coreClock = arr.last
|
|
grid.addRow(with: arr)
|
|
num += 1
|
|
}
|
|
if let value = value.memoryClock {
|
|
let arr = keyValueRow("\(localizedString("Memory clock")):", "\(value)MHz")
|
|
self.memoryClock = arr.last
|
|
grid.addRow(with: arr)
|
|
num += 1
|
|
}
|
|
|
|
if let value = value.temperature {
|
|
let arr = keyValueRow("\(localizedString("Temperature")):", Kit.temperature(Double(value)))
|
|
self.temperature = arr.last
|
|
grid.addRow(with: arr)
|
|
num += 1
|
|
}
|
|
if let value = value.utilization {
|
|
let arr = keyValueRow("\(localizedString("Utilization")):", "\(Int(value*100))%")
|
|
self.utilization = arr.last
|
|
grid.addRow(with: arr)
|
|
num += 1
|
|
}
|
|
if let value = value.renderUtilization {
|
|
let arr = keyValueRow("\(localizedString("Render utilization")):", "\(Int(value*100))%")
|
|
self.renderUtilization = arr.last
|
|
grid.addRow(with: arr)
|
|
num += 1
|
|
}
|
|
if let value = value.tilerUtilization {
|
|
let arr = keyValueRow("\(localizedString("Tiler utilization")):", "\(Int(value*100))%")
|
|
self.tilerUtilization = arr.last
|
|
grid.addRow(with: arr)
|
|
num += 1
|
|
}
|
|
|
|
self.setFrameSize(NSSize(width: self.frame.width, height: (16 * num) + Constants.Popup.margins))
|
|
grid.setFrameSize(NSSize(width: grid.frame.width, height: self.frame.height - Constants.Popup.margins))
|
|
self.addSubview(grid)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func keyValueRow(_ key: String, _ value: String) -> [NSTextField] {
|
|
return [
|
|
LabelField(frame: NSRect(x: 0, y: 0, width: 0, height: 16), key),
|
|
ValueField(frame: NSRect(x: 0, y: 0, width: 0, height: 16), value)
|
|
]
|
|
}
|
|
|
|
public func update(_ gpu: GPU_Info) {
|
|
self.status?.stringValue = gpu.state ? localizedString("Active") : localizedString("Non active")
|
|
|
|
if let value = gpu.fanSpeed {
|
|
self.fanSpeed?.stringValue = "\(value)%"
|
|
}
|
|
if let value = gpu.coreClock {
|
|
self.coreClock?.stringValue = "\(value)MHz"
|
|
}
|
|
if let value = gpu.memoryClock {
|
|
self.memoryClock?.stringValue = "\(value)MHz"
|
|
}
|
|
|
|
if let value = gpu.temperature {
|
|
self.temperature?.stringValue = Kit.temperature(Double(value))
|
|
}
|
|
if let value = gpu.utilization {
|
|
self.utilization?.stringValue = "\(Int(value*100))%"
|
|
}
|
|
if let value = gpu.renderUtilization {
|
|
self.renderUtilization?.stringValue = "\(Int(value*100))%"
|
|
}
|
|
if let value = gpu.tilerUtilization {
|
|
self.tilerUtilization?.stringValue = "\(Int(value*100))%"
|
|
}
|
|
}
|
|
}
|