diff --git a/ModuleKit/Constants.swift b/ModuleKit/Constants.swift index 66a90b43..e4d7e817 100644 --- a/ModuleKit/Constants.swift +++ b/ModuleKit/Constants.swift @@ -34,6 +34,7 @@ public struct Widget_c_s { } } public let margin: CGPoint = CGPoint(x: 2, y: 2) + public let spacing: CGFloat = 2 } public struct Constants { diff --git a/ModuleKit/Widgets/PieChart.swift b/ModuleKit/Widgets/PieChart.swift new file mode 100644 index 00000000..d4660312 --- /dev/null +++ b/ModuleKit/Widgets/PieChart.swift @@ -0,0 +1,142 @@ +// +// PieChart.swift +// ModuleKit +// +// Created by Serhiy Mytrovtsiy on 30/11/2020. +// Using Swift 5.0. +// Running on macOS 10.15. +// +// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import StatsKit + +public class PieChart: Widget { + private var labelState: Bool = true + + private let store: UnsafePointer? + private var chart: PieChartView = PieChartView( + frame: NSRect( + x: Constants.Widget.margin.x, + y: Constants.Widget.margin.y, + width: Constants.Widget.height, + height: Constants.Widget.height + ), + segments: [], filled: true, drawValue: false + ) + private var labelView: NSView? = nil + + private let size: CGFloat = Constants.Widget.height - (Constants.Widget.margin.y*2) + (Constants.Widget.margin.x*2) + + public init(preview: Bool, title: String, config: NSDictionary?, store: UnsafePointer?) { + var widgetTitle: String = title + self.store = store + if config != nil { + if let titleFromConfig = config!["Title"] as? String { + widgetTitle = titleFromConfig + } + } + + super.init(frame: CGRect( + x: Constants.Widget.margin.x, + y: Constants.Widget.margin.y, + width: self.size, + height: Constants.Widget.height - (Constants.Widget.margin.y*2) + )) + + self.preview = preview + self.title = widgetTitle + self.type = .pieChart + self.wantsLayer = true + self.canDrawConcurrently = true + + if let store = self.store { + self.labelState = store.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState) + } + + if self.preview { + if self.title == "CPU" { + self.chart.setSegments([ + circle_segment(value: 0.16, color: NSColor.systemRed), + circle_segment(value: 0.28, color: NSColor.systemBlue) + ]) + } else if self.title == "RAM" { + self.chart.setSegments([ + circle_segment(value: 0.36, color: NSColor.systemBlue), + circle_segment(value: 0.12, color: NSColor.systemOrange), + circle_segment(value: 0.08, color: NSColor.systemPink) + ]) + } + } + + self.draw() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func draw() { + let x: CGFloat = self.labelState ? 8 + Constants.Widget.spacing : 0 + + self.labelView = WidgetLabelView(self.title, height: self.frame.height) + self.labelView!.isHidden = !self.labelState + + self.addSubview(self.labelView!) + self.addSubview(self.chart) + + var frame = self.chart.frame + frame = NSRect(x: x, y: 0, width: self.frame.size.height, height: self.frame.size.height) + self.chart.frame = frame + + self.setFrameSize(NSSize(width: self.size + x, height: self.frame.size.height)) + } + + public func setValue(_ segments: [circle_segment]) { + DispatchQueue.main.async(execute: { + self.chart.setSegments(segments) + }) + } + + // MARK: - Settings + + public override func settings(superview: NSView) { + let rowHeight: CGFloat = 30 + let height: CGFloat = ((rowHeight + Constants.Settings.margin) * 1) + Constants.Settings.margin + superview.setFrameSize(NSSize(width: superview.frame.width, height: height)) + + let view: NSView = NSView(frame: NSRect( + x: Constants.Settings.margin, + y: Constants.Settings.margin, + width: superview.frame.width - (Constants.Settings.margin*2), + height: superview.frame.height - (Constants.Settings.margin*2) + )) + + view.addSubview(ToggleTitleRow( + frame: NSRect(x: 0, y: 0, width: view.frame.width, height: rowHeight), + title: LocalizedString("Label"), + action: #selector(toggleLabel), + state: self.labelState + )) + + superview.addSubview(view) + } + + @objc private func toggleLabel(_ sender: NSControl) { + var state: NSControl.StateValue? = nil + if #available(OSX 10.15, *) { + state = sender is NSSwitch ? (sender as! NSSwitch).state: nil + } else { + state = sender is NSButton ? (sender as! NSButton).state: nil + } + + self.labelState = state! == .on ? true : false + self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState) + + let x = self.labelState ? 6 + Constants.Widget.spacing : 0 + self.labelView!.isHidden = !self.labelState + self.chart.setFrameOrigin(NSPoint(x: x, y: 0)) + self.setWidth(self.labelState ? self.size+x : self.size) + } +} diff --git a/ModuleKit/settings.swift b/ModuleKit/settings.swift index 4e684374..b4b48bae 100644 --- a/ModuleKit/settings.swift +++ b/ModuleKit/settings.swift @@ -285,6 +285,7 @@ open class Settings: NSView, Settings_p { self.widgetSettingsView?.removeFromSuperview() self.moduleSettingsView?.removeFromSuperview() + self.widgetSettingsView = nil self.addWidgetSettings() if self.moduleSettings != nil { diff --git a/ModuleKit/widget.swift b/ModuleKit/widget.swift index 45075213..94ed59ee 100644 --- a/ModuleKit/widget.swift +++ b/ModuleKit/widget.swift @@ -57,6 +57,7 @@ public enum widget_t: String { case mini = "mini" case lineChart = "line_chart" case barChart = "bar_chart" + case pieChart = "pie_chart" case speed = "speed" case battery = "battery" case sensors = "sensors" @@ -83,6 +84,7 @@ open class Widget: NSView, Widget_p { case .mini: return "Mini" case .lineChart: return "Line chart" case .barChart: return "Bar chart" + case .pieChart: return "Pie chart" case .speed: return "Speed" case .battery: return "Battery" case .sensors: return "Text" @@ -141,6 +143,9 @@ func LoadWidget(_ type: widget_t, preview: Bool, name: String, config: NSDiction case .barChart: widget = BarChart(preview: preview, title: name, config: widgetConfig, store: store) break + case .pieChart: + widget = PieChart(preview: preview, title: name, config: widgetConfig, store: store) + break case .speed: widget = SpeedWidget(preview: preview, title: name, config: widgetConfig, store: store) break diff --git a/Modules/CPU/config.plist b/Modules/CPU/config.plist index 46a68fdf..423d2b48 100644 --- a/Modules/CPU/config.plist +++ b/Modules/CPU/config.plist @@ -55,6 +55,13 @@ Order 2 + pie_chart + + Default + + Order + 3 + diff --git a/Modules/CPU/main.swift b/Modules/CPU/main.swift index f822e1a0..8d481d07 100644 --- a/Modules/CPU/main.swift +++ b/Modules/CPU/main.swift @@ -132,5 +132,11 @@ public class CPU: Module { if let widget = self.widget as? BarChart { widget.setValue(self.usagePerCoreState ? value!.usagePerCore : [value!.totalUsage]) } + if let widget = self.widget as? PieChart { + widget.setValue([ + circle_segment(value: value!.systemLoad, color: NSColor.systemRed), + circle_segment(value: value!.userLoad, color: NSColor.systemBlue) + ]) + } } } diff --git a/Modules/CPU/popup.swift b/Modules/CPU/popup.swift index 16e23ccf..7d0e820b 100644 --- a/Modules/CPU/popup.swift +++ b/Modules/CPU/popup.swift @@ -31,7 +31,7 @@ internal class Popup: NSView, Popup_p { private var idleField: NSTextField? = nil private var chart: LineChartView? = nil - private var circle: CircleGraphView? = nil + private var circle: PieChartView? = nil private var temperatureCircle: HalfCircleGraphView? = nil private var frequencyCircle: HalfCircleGraphView? = nil private var initialized: Bool = false @@ -118,7 +118,12 @@ internal class Popup: NSView, Popup_p { let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.dashboardHeight)) let container: NSView = NSView(frame: NSRect(x: 0, y: 10, width: view.frame.width, height: self.dashboardHeight-20)) - self.circle = CircleGraphView(frame: NSRect(x: (container.frame.width - container.frame.height)/2, y: 0, width: container.frame.height, height: container.frame.height), segments: []) + self.circle = PieChartView(frame: NSRect( + x: (container.frame.width - container.frame.height)/2, + y: 0, + width: container.frame.height, + height: container.frame.height + ), segments: [], drawValue: true) self.circle!.toolTip = LocalizedString("CPU usage") container.addSubview(self.circle!) diff --git a/Modules/Memory/config.plist b/Modules/Memory/config.plist index 95b7339a..4865eded 100644 --- a/Modules/Memory/config.plist +++ b/Modules/Memory/config.plist @@ -53,6 +53,13 @@ Order 2 + pie_chart + + Default + + Order + 3 + memory Default @@ -63,7 +70,7 @@ 51383185408,198466408448 Order - 3 + 4 diff --git a/Modules/Memory/main.swift b/Modules/Memory/main.swift index 01c2ba99..1d888eab 100644 --- a/Modules/Memory/main.swift +++ b/Modules/Memory/main.swift @@ -104,6 +104,14 @@ public class Memory: Module { widget.setValue([value!.usage]) widget.setPressure(value?.pressureLevel ?? 0) } + if let widget = self.widget as? PieChart { + let total: Double = value?.total ?? 1 + widget.setValue([ + circle_segment(value: value!.active/total, color: NSColor.systemBlue), + circle_segment(value: value!.wired/total, color: NSColor.systemOrange), + circle_segment(value: value!.compressed/total, color: NSColor.systemPink) + ]) + } if let widget = self.widget as? MemoryWidget { widget.setValue((Int64(value!.free), Int64(value!.used))) } diff --git a/Modules/Memory/popup.swift b/Modules/Memory/popup.swift index 3ba16a00..76db4a6a 100644 --- a/Modules/Memory/popup.swift +++ b/Modules/Memory/popup.swift @@ -34,7 +34,7 @@ internal class Popup: NSView, Popup_p { private var compressedField: NSTextField? = nil private var chart: LineChartView? = nil - private var circle: CircleGraphView? = nil + private var circle: PieChartView? = nil private var level: PressureView? = nil private var initialized: Bool = false private var processesInitialized: Bool = false @@ -117,7 +117,12 @@ internal class Popup: NSView, Popup_p { let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.dashboardHeight, width: self.frame.width, height: self.dashboardHeight)) let container: NSView = NSView(frame: NSRect(x: 0, y: 10, width: view.frame.width, height: self.dashboardHeight-20)) - self.circle = CircleGraphView(frame: NSRect(x: (container.frame.width - container.frame.height)/2, y: 0, width: container.frame.height, height: container.frame.height), segments: []) + self.circle = PieChartView(frame: NSRect( + x: (container.frame.width - container.frame.height)/2, + y: 0, + width: container.frame.height, + height: container.frame.height + ), segments: [], drawValue: true) self.circle!.toolTip = LocalizedString("Memory usage") container.addSubview(self.circle!) diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index f9784160..1094923c 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 9A0C82EE2446124800FAE3D4 /* SystemKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7D0CB62444C2C800B09070 /* SystemKit.swift */; }; 9A1A7ABA24561F0B00A84F7A /* BarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1A7AB924561F0B00A84F7A /* BarChart.swift */; }; 9A1D5E4B25235C8100B82BFC /* helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1D5E4A25235C8100B82BFC /* helpers.swift */; }; + 9A20E6DA2575555100AC2302 /* PieChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A20E6D92575555100AC2302 /* PieChart.swift */; }; 9A27D4FD2538A3E5001BB651 /* Repeat in Frameworks */ = {isa = PBXBuildFile; productRef = 9A27D4FC2538A3E5001BB651 /* Repeat */; }; 9A27D5352538A456001BB651 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 9A27D5342538A456001BB651 /* Reachability */; }; 9A34353B243E278D006B19F9 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A34353A243E278D006B19F9 /* main.swift */; }; @@ -376,6 +377,7 @@ 9A141101229E721200D29793 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9A1A7AB924561F0B00A84F7A /* BarChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarChart.swift; sourceTree = ""; }; 9A1D5E4A25235C8100B82BFC /* helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = helpers.swift; sourceTree = ""; }; + 9A20E6D92575555100AC2302 /* PieChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PieChart.swift; sourceTree = ""; }; 9A27D4A925389EFD001BB651 /* Stats.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Stats.entitlements; sourceTree = ""; }; 9A343527243E26A0006B19F9 /* LaunchAtLogin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LaunchAtLogin.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9A343535243E26A0006B19F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -697,6 +699,7 @@ 9A7C61B32440DF810032695D /* Mini.swift */, 9AA64263244B94F300416A33 /* LineChart.swift */, 9A1A7AB924561F0B00A84F7A /* BarChart.swift */, + 9A20E6D92575555100AC2302 /* PieChart.swift */, 9A3E17E7247AA8E100449CD1 /* Speed.swift */, 9ABFF911248BF39500C9041A /* Battery.swift */, 9AE29AFD249A82B70071B02D /* Sensors.swift */, @@ -1498,6 +1501,7 @@ 9A3E17E8247AA8E100449CD1 /* Speed.swift in Sources */, 9AA64264244B94F300416A33 /* LineChart.swift in Sources */, 9A1A7ABA24561F0B00A84F7A /* BarChart.swift in Sources */, + 9A20E6DA2575555100AC2302 /* PieChart.swift in Sources */, 9A944D55244920690058F32A /* reader.swift in Sources */, 9A7C61B42440DF810032695D /* Mini.swift in Sources */, 9AE29AFE249A82B70071B02D /* Sensors.swift in Sources */, diff --git a/StatsKit/Charts.swift b/StatsKit/Charts.swift index 668a2fc0..ad02ff0c 100644 --- a/StatsKit/Charts.swift +++ b/StatsKit/Charts.swift @@ -227,12 +227,18 @@ public class NetworkChartView: NSView { } } -public class CircleGraphView: NSView { +public class PieChartView: NSView { + private var filled: Bool = false + private var drawValue: Bool = false + private var value: Double? = nil private var segments: [circle_segment] = [] - public init(frame: NSRect, segments: [circle_segment]) { + public init(frame: NSRect, segments: [circle_segment], filled: Bool = false, drawValue: Bool = false) { + self.filled = filled + self.drawValue = drawValue self.segments = segments + super.init(frame: frame) } @@ -241,7 +247,7 @@ public class CircleGraphView: NSView { } public override func draw(_ rect: CGRect) { - let arcWidth: CGFloat = 7.0 + let arcWidth: CGFloat = self.filled ? min(rect.width, rect.height) / 2 : 7 let fullCircle = 2 * CGFloat.pi var segments = self.segments let totalAmount = segments.reduce(0) { $0 + $1.value } @@ -271,7 +277,7 @@ public class CircleGraphView: NSView { previousAngle = currentAngle } - if let value = self.value { + if let value = self.value, self.drawValue { let stringAttributes = [ NSAttributedString.Key.font: NSFont.systemFont(ofSize: 15, weight: .regular), NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor, @@ -280,7 +286,7 @@ public class CircleGraphView: NSView { let percentage = "\(Int(value*100))%" let width: CGFloat = percentage.widthOfString(usingFont: NSFont.systemFont(ofSize: 15)) - let rect = CGRect(x: (self.frame.width-width)/2, y: (self.frame.height-12)/2, width: width, height: 12) + let rect = CGRect(x: (self.frame.width-width)/2, y: (self.frame.height-11)/2, width: width, height: 12) let str = NSAttributedString.init(string: percentage, attributes: stringAttributes) str.draw(with: rect) } diff --git a/StatsKit/helpers.swift b/StatsKit/helpers.swift index 470b0330..abf1b9b3 100644 --- a/StatsKit/helpers.swift +++ b/StatsKit/helpers.swift @@ -780,3 +780,46 @@ public class CAText: CATextLayer { return value.widthOfString(usingFont: self.font as! NSFont).rounded(.up) + add } } + +public class WidgetLabelView: NSView { + private var title: String + + public init(_ title: String, height: CGFloat) { + self.title = title + + super.init(frame: NSRect( + x: 0, + y: 0, + width: 6, + height: height + )) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + 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 + ] + + let title = self.title.prefix(3) + let letterHeight = self.frame.height / 3 + let letterWidth: CGFloat = self.frame.height / CGFloat(title.count) + + var yMargin: CGFloat = 0 + for char in title.uppercased().reversed() { + let rect = CGRect(x: 0, y: yMargin, width: letterWidth, height: letterHeight-1) + let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes) + str.draw(with: rect) + yMargin += letterHeight + } + } +}