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.
466 lines
17 KiB
Swift
466 lines
17 KiB
Swift
//
|
|
// Sensors.swift
|
|
// Kit
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 17/06/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
|
|
public struct Stack_t: KeyValue_p {
|
|
public var key: String
|
|
public var value: String
|
|
|
|
var index: Int {
|
|
get { Store.shared.int(key: "stack_\(self.key)_index", defaultValue: -1) }
|
|
set { Store.shared.set(key: "stack_\(self.key)_index", value: newValue) }
|
|
}
|
|
|
|
public init(key: String, value: String) {
|
|
self.key = key
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
public class StackWidget: WidgetWrapper {
|
|
private var modeState: StackMode = .auto
|
|
private var fixedSizeState: Bool = false
|
|
private var monospacedFontState: Bool = false
|
|
private var alignmentState: String = "left"
|
|
|
|
private var values: [Stack_t] = []
|
|
|
|
private var oneRowWidth: CGFloat = 45
|
|
private var twoRowWidth: CGFloat = 32
|
|
|
|
private let orderTableView: OrderTableView
|
|
|
|
private var alignment: NSTextAlignment {
|
|
if let alignmentPair = Alignments.first(where: { $0.key == self.alignmentState }) {
|
|
return alignmentPair.additional as? NSTextAlignment ?? .left
|
|
}
|
|
return .left
|
|
}
|
|
|
|
public init(title: String, config: NSDictionary?, preview: Bool = false) {
|
|
if let config, preview {
|
|
if let previewConfig = config["Preview"] as? NSDictionary {
|
|
if let value = previewConfig["Values"] as? String {
|
|
for (i, value) in value.split(separator: ",").enumerated() {
|
|
self.values.append(Stack_t(key: "\(i)", value: String(value)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.orderTableView = OrderTableView(&self.values)
|
|
|
|
super.init(.stack, title: title, frame: CGRect(
|
|
x: 0,
|
|
y: Constants.Widget.margin.y,
|
|
width: Constants.Widget.width,
|
|
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
|
|
))
|
|
|
|
if !preview {
|
|
self.modeState = StackMode(rawValue: Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_mode", defaultValue: self.modeState.rawValue)) ?? .auto
|
|
self.fixedSizeState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_size", defaultValue: self.fixedSizeState)
|
|
self.monospacedFontState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_monospacedFont", defaultValue: self.monospacedFontState)
|
|
self.alignmentState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_alignment", defaultValue: self.alignmentState)
|
|
}
|
|
|
|
self.orderTableView.reorderCallback = { [weak self] in
|
|
self?.display()
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public override func draw(_ dirtyRect: NSRect) {
|
|
super.draw(dirtyRect)
|
|
|
|
var values: [Stack_t] = []
|
|
var mode: StackMode = .auto
|
|
self.queue.sync {
|
|
values = self.values
|
|
mode = self.modeState
|
|
}
|
|
|
|
guard !values.isEmpty else {
|
|
self.setWidth(0)
|
|
return
|
|
}
|
|
|
|
let num: Int = Int(round(Double(values.count) / 2))
|
|
var totalWidth: CGFloat = Constants.Widget.spacing // opening space
|
|
var x: CGFloat = Constants.Widget.spacing
|
|
|
|
var i = 0
|
|
while i < values.count {
|
|
switch mode {
|
|
case .auto, .twoRows:
|
|
let firstElement: Stack_t = values[i]
|
|
let secondElement: Stack_t? = values.indices.contains(i+1) ? values[i+1] : nil
|
|
|
|
var width: CGFloat = 0
|
|
if mode == .auto && secondElement == nil {
|
|
width += self.drawOneRow(x, firstElement)
|
|
} else {
|
|
width += self.drawTwoRows(x, firstElement, secondElement)
|
|
}
|
|
|
|
x += width
|
|
totalWidth += width
|
|
|
|
if num != 1 && (i/2) != num {
|
|
x += Constants.Widget.spacing
|
|
totalWidth += Constants.Widget.spacing
|
|
}
|
|
|
|
i += 1
|
|
case .oneRow:
|
|
let width = self.drawOneRow(x, values[i])
|
|
|
|
x += width
|
|
totalWidth += width
|
|
|
|
// add margins between columns
|
|
if values.count != 1 && i != values.count {
|
|
x += Constants.Widget.spacing
|
|
totalWidth += Constants.Widget.spacing
|
|
}
|
|
}
|
|
|
|
i += 1
|
|
}
|
|
totalWidth += Constants.Widget.spacing // closing space
|
|
|
|
guard abs(self.frame.width - totalWidth) > 2 else { return }
|
|
self.setWidth(totalWidth)
|
|
}
|
|
|
|
private func drawOneRow(_ x: CGFloat, _ element: Stack_t) -> CGFloat {
|
|
var monospacedFontState: Bool = false
|
|
var fixedSizeState: Bool = false
|
|
var alignment: NSTextAlignment = .left
|
|
self.queue.sync {
|
|
monospacedFontState = self.monospacedFontState
|
|
fixedSizeState = self.fixedSizeState
|
|
alignment = self.alignment
|
|
}
|
|
|
|
var font: NSFont = NSFont.systemFont(ofSize: 13, weight: .regular)
|
|
if monospacedFontState {
|
|
font = NSFont.monospacedDigitSystemFont(ofSize: 13, weight: .regular)
|
|
}
|
|
|
|
let style = NSMutableParagraphStyle()
|
|
style.alignment = alignment
|
|
|
|
var width: CGFloat = self.oneRowWidth
|
|
if !fixedSizeState {
|
|
width = element.value.widthOfString(usingFont: font).rounded(.up) + 2
|
|
}
|
|
|
|
let rect = CGRect(x: x, y: (Constants.Widget.height-13)/2, width: width, height: 13)
|
|
let str = NSAttributedString.init(string: element.value, attributes: [
|
|
NSAttributedString.Key.font: font,
|
|
NSAttributedString.Key.foregroundColor: NSColor.textColor,
|
|
NSAttributedString.Key.paragraphStyle: style
|
|
])
|
|
str.draw(with: rect)
|
|
|
|
return width
|
|
}
|
|
|
|
private func drawTwoRows(_ x: CGFloat, _ topElement: Stack_t, _ bottomElement: Stack_t?) -> CGFloat {
|
|
let rowHeight: CGFloat = self.frame.height / 2
|
|
var monospacedFontState: Bool = false
|
|
var fixedSizeState: Bool = false
|
|
var alignment: NSTextAlignment = .left
|
|
self.queue.sync {
|
|
monospacedFontState = self.monospacedFontState
|
|
fixedSizeState = self.fixedSizeState
|
|
alignment = self.alignment
|
|
}
|
|
|
|
var font: NSFont
|
|
if monospacedFontState {
|
|
font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .light)
|
|
} else {
|
|
font = NSFont.systemFont(ofSize: 10, weight: .light)
|
|
}
|
|
let style = NSMutableParagraphStyle()
|
|
style.alignment = alignment
|
|
|
|
let attributes = [
|
|
NSAttributedString.Key.font: font,
|
|
NSAttributedString.Key.foregroundColor: NSColor.textColor,
|
|
NSAttributedString.Key.paragraphStyle: style
|
|
]
|
|
|
|
var width: CGFloat = self.twoRowWidth
|
|
if !fixedSizeState {
|
|
let firstRowWidth = topElement.value.widthOfString(usingFont: font)
|
|
let secondRowWidth = bottomElement?.value.widthOfString(usingFont: font) ?? 0
|
|
width = max(20, max(firstRowWidth, secondRowWidth)).rounded(.up) + 2
|
|
}
|
|
|
|
var rect = CGRect(x: x, y: rowHeight+1, width: width, height: rowHeight)
|
|
var str = NSAttributedString.init(string: topElement.value, attributes: attributes)
|
|
str.draw(with: rect)
|
|
|
|
if bottomElement != nil {
|
|
rect = CGRect(x: x, y: 1, width: width, height: rowHeight)
|
|
str = NSAttributedString.init(string: bottomElement!.value, attributes: attributes)
|
|
str.draw(with: rect)
|
|
}
|
|
|
|
return width
|
|
}
|
|
|
|
public func setValues(_ values: [Stack_t]) {
|
|
DispatchQueue.main.async(execute: {
|
|
var tableNeedsToBeUpdated: Bool = false
|
|
|
|
values.forEach { (p: Stack_t) in
|
|
if let idx = self.values.firstIndex(where: { $0.key == p.key }) {
|
|
self.values[idx].value = p.value
|
|
return
|
|
}
|
|
tableNeedsToBeUpdated = true
|
|
self.values.append(p)
|
|
}
|
|
|
|
let diff = self.values.filter({ v in values.contains(where: { $0.key == v.key }) })
|
|
if diff.count != self.values.count {
|
|
tableNeedsToBeUpdated = true
|
|
}
|
|
self.values = diff.sorted(by: { $0.index < $1.index })
|
|
|
|
if tableNeedsToBeUpdated {
|
|
self.orderTableView.update()
|
|
}
|
|
self.display()
|
|
})
|
|
}
|
|
|
|
// MARK: - Settings
|
|
|
|
public override func settings() -> NSView {
|
|
let view = SettingsContainerView()
|
|
|
|
var rows = [
|
|
PreferencesRow(localizedString("Display mode"), component: selectView(
|
|
action: #selector(self.changeDisplayMode),
|
|
items: SensorsWidgetMode,
|
|
selected: self.modeState.rawValue
|
|
)),
|
|
PreferencesRow(localizedString("Monospaced font"), component: switchView(
|
|
action: #selector(self.toggleMonospacedFont),
|
|
state: self.monospacedFontState
|
|
)),
|
|
PreferencesRow(localizedString("Alignment"), component: selectView(
|
|
action: #selector(self.toggleAlignment),
|
|
items: Alignments,
|
|
selected: self.alignmentState
|
|
))
|
|
]
|
|
if self.title != "Clock" {
|
|
rows.append(PreferencesRow(localizedString("Static width"), component: switchView(
|
|
action: #selector(self.toggleSize),
|
|
state: self.fixedSizeState
|
|
)))
|
|
}
|
|
view.addArrangedSubview(PreferencesSection(rows))
|
|
|
|
view.addArrangedSubview(self.orderTableView)
|
|
|
|
return view
|
|
}
|
|
|
|
@objc private func changeDisplayMode(_ sender: NSMenuItem) {
|
|
guard let key = sender.representedObject as? String else { return }
|
|
self.modeState = StackMode(rawValue: key) ?? .auto
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_mode", value: key)
|
|
self.display()
|
|
}
|
|
|
|
@objc private func toggleSize(_ sender: NSControl) {
|
|
self.fixedSizeState = controlState(sender)
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_size", value: self.fixedSizeState)
|
|
self.display()
|
|
}
|
|
|
|
@objc private func toggleMonospacedFont(_ sender: NSControl) {
|
|
self.monospacedFontState = controlState(sender)
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_monospacedFont", value: self.monospacedFontState)
|
|
self.display()
|
|
}
|
|
|
|
@objc private func toggleAlignment(_ sender: NSMenuItem) {
|
|
guard let key = sender.representedObject as? String else { return }
|
|
if let newAlignment = Alignments.first(where: { $0.key == key }) {
|
|
self.alignmentState = newAlignment.key
|
|
}
|
|
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_alignment", value: key)
|
|
self.display()
|
|
}
|
|
}
|
|
|
|
private class OrderTableView: NSView, NSTableViewDelegate, NSTableViewDataSource {
|
|
private let scrollView = NSScrollView()
|
|
private let tableView = NSTableView()
|
|
private var dragDropType = NSPasteboard.PasteboardType(rawValue: "\(Bundle.main.bundleIdentifier!).sensors-row")
|
|
|
|
fileprivate var reorderCallback: () -> Void = {}
|
|
private let list: UnsafeMutablePointer<[Stack_t]>
|
|
|
|
init(_ list: UnsafeMutablePointer<[Stack_t]>) {
|
|
self.list = list
|
|
|
|
super.init(frame: NSRect.zero)
|
|
|
|
self.wantsLayer = true
|
|
self.layer?.cornerRadius = 3
|
|
|
|
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
self.scrollView.documentView = self.tableView
|
|
self.scrollView.hasHorizontalScroller = false
|
|
self.scrollView.hasVerticalScroller = true
|
|
self.scrollView.autohidesScrollers = true
|
|
self.scrollView.backgroundColor = NSColor.clear
|
|
self.scrollView.drawsBackground = true
|
|
|
|
self.tableView.frame = self.scrollView.bounds
|
|
self.tableView.delegate = self
|
|
self.tableView.dataSource = self
|
|
self.tableView.headerView = nil
|
|
self.tableView.backgroundColor = NSColor.clear
|
|
self.tableView.columnAutoresizingStyle = .firstColumnOnlyAutoresizingStyle
|
|
self.tableView.registerForDraggedTypes([dragDropType])
|
|
self.tableView.gridColor = .gridColor
|
|
self.tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
|
|
if #available(macOS 11.0, *) {
|
|
self.tableView.style = .plain
|
|
}
|
|
|
|
self.tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "name")))
|
|
|
|
self.addSubview(self.scrollView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
self.scrollView.leftAnchor.constraint(equalTo: self.leftAnchor),
|
|
self.scrollView.rightAnchor.constraint(equalTo: self.rightAnchor),
|
|
self.scrollView.topAnchor.constraint(equalTo: self.topAnchor),
|
|
self.scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
|
|
|
self.heightAnchor.constraint(equalToConstant: 120)
|
|
])
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
fileprivate func update() {
|
|
self.tableView.reloadData()
|
|
}
|
|
|
|
func numberOfRows(in tableView: NSTableView) -> Int {
|
|
return list.pointee.count
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
|
if !self.list.pointee.indices.contains(row) { return nil }
|
|
let item = self.list.pointee[row]
|
|
|
|
let text: NSTextField = NSTextField()
|
|
text.drawsBackground = false
|
|
text.isBordered = false
|
|
text.isEditable = false
|
|
text.isSelectable = false
|
|
text.translatesAutoresizingMaskIntoConstraints = false
|
|
text.identifier = NSUserInterfaceItemIdentifier(item.key)
|
|
|
|
switch tableColumn?.identifier.rawValue {
|
|
case "name": text.stringValue = item.key
|
|
default: break
|
|
}
|
|
|
|
text.sizeToFit()
|
|
|
|
let cell = NSTableCellView()
|
|
cell.addSubview(text)
|
|
|
|
NSLayoutConstraint.activate([
|
|
text.widthAnchor.constraint(equalTo: cell.widthAnchor),
|
|
text.centerYAnchor.constraint(equalTo: cell.centerYAnchor)
|
|
])
|
|
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
|
|
let item = NSPasteboardItem()
|
|
item.setString(String(row), forType: self.dragDropType)
|
|
return item
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
|
|
if dropOperation == .above {
|
|
return .move
|
|
}
|
|
return []
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {
|
|
var oldIndexes = [Int]()
|
|
info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) { dragItem, _, _ in
|
|
if let str = (dragItem.item as! NSPasteboardItem).string(forType: self.dragDropType), let index = Int(str) {
|
|
oldIndexes.append(index)
|
|
}
|
|
}
|
|
|
|
var oldIndexOffset = 0
|
|
var newIndexOffset = 0
|
|
|
|
tableView.beginUpdates()
|
|
for oldIndex in oldIndexes {
|
|
if oldIndex < row {
|
|
let currentIdx = oldIndex + oldIndexOffset
|
|
let newIdx = row - 1
|
|
|
|
self.list.pointee[currentIdx].index = newIdx
|
|
self.list.pointee[newIdx].index = currentIdx
|
|
|
|
oldIndexOffset -= 1
|
|
} else {
|
|
let currentIdx = oldIndex
|
|
let newIdx = row + newIndexOffset
|
|
|
|
self.list.pointee[currentIdx].index = newIdx
|
|
self.list.pointee[newIdx].index = currentIdx
|
|
|
|
newIndexOffset += 1
|
|
}
|
|
self.list.pointee = self.list.pointee.sorted(by: { $0.index < $1.index })
|
|
self.reorderCallback()
|
|
tableView.reloadData()
|
|
}
|
|
tableView.endUpdates()
|
|
|
|
return true
|
|
}
|
|
}
|