From e524df7d5eaa0e6191464e764bbbe91bf05ceeb3 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Thu, 21 Jan 2021 21:56:06 +0100 Subject: [PATCH] feat: add new widget `Network Chart` (#189) for Network module. --- ModuleKit/Widgets/NetworkChart.swift | 191 +++++++++++++++++++++++++++ ModuleKit/reader.swift | 17 ++- ModuleKit/widget.swift | 5 + Modules/Net/config.plist | 7 + Modules/Net/main.swift | 8 +- Stats.xcodeproj/project.pbxproj | 4 + StatsKit/Charts.swift | 38 +++--- StatsKit/updater.swift | 10 +- 8 files changed, 250 insertions(+), 30 deletions(-) create mode 100644 ModuleKit/Widgets/NetworkChart.swift diff --git a/ModuleKit/Widgets/NetworkChart.swift b/ModuleKit/Widgets/NetworkChart.swift new file mode 100644 index 00000000..e2e5a152 --- /dev/null +++ b/ModuleKit/Widgets/NetworkChart.swift @@ -0,0 +1,191 @@ +// +// NetworkChart.swift +// ModuleKit +// +// Created by Serhiy Mytrovtsiy on 19/01/2021. +// Using Swift 5.0. +// Running on macOS 11.1. +// +// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import StatsKit + +public class NetworkChart: Widget { + private var boxState: Bool = false + private var frameState: Bool = false + + private let store: UnsafePointer? + private var chart: NetworkChartView = NetworkChartView( + frame: NSRect( + x: 0, + y: 0, + width: 34, + height: Constants.Widget.height - (2*Constants.Widget.margin.y) + ), + num: 60, minMax: false + ) + private let width: CGFloat = 34 + + private var boxSettingsView: NSView? = nil + private var frameSettingsView: NSView? = nil + + 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.width + (2*Constants.Widget.margin.x), + height: Constants.Widget.height - (2*Constants.Widget.margin.y) + )) + + self.preview = preview + self.title = widgetTitle + self.type = .networkChart + self.wantsLayer = true + self.canDrawConcurrently = true + + if self.store != nil && !preview { + self.boxState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_box", defaultValue: self.boxState) + self.frameState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_frame", defaultValue: self.frameState) + } + + if preview { + var list: [(Double, Double)] = [] + for _ in 0..<60 { + list.append((Double.random(in: 0..<23), Double.random(in: 0..<23))) + } + self.chart.points = list + } + } + + 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 } + + let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1) + let offset = lineWidth / 2 + let boxSize: CGSize = CGSize(width: self.width - (Constants.Widget.margin.x*2), height: self.frame.size.height) + + let box = NSBezierPath(roundedRect: NSRect( + x: offset, + y: offset, + width: boxSize.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() + } + + context.saveGState() + + let chartFrame = NSRect( + x: offset, + y: 1, + width: box.bounds.width, + height: box.bounds.height-1 + ) + 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(upload: Double, download: Double) { + DispatchQueue.main.async(execute: { + self.chart.addValue(upload: upload, download: download) + self.display() + }) + } + + // MARK: - Settings + + public override func settings(superview: NSView) { + let rowHeight: CGFloat = 30 + let settingsNumber: CGFloat = 2 + let height: CGFloat = ((rowHeight + Constants.Settings.margin) * settingsNumber) + 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))) + + self.boxSettingsView = ToggleTitleRow( + frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 1, width: view.frame.width, height: rowHeight), + title: LocalizedString("Box"), + action: #selector(toggleBox), + state: self.boxState + ) + view.addSubview(self.boxSettingsView!) + + self.frameSettingsView = ToggleTitleRow( + frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 0, width: view.frame.width, height: rowHeight), + title: LocalizedString("Frame"), + action: #selector(toggleFrame), + state: self.frameState + ) + view.addSubview(self.frameSettingsView!) + + superview.addSubview(view) + } + + @objc private func toggleBox(_ 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.boxState = state! == .on ? true : false + self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState) + + if self.frameState { + FindAndToggleNSControlState(self.frameSettingsView, state: .off) + self.frameState = false + self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState) + } + + self.display() + } + + @objc private func toggleFrame(_ 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.frameState = state! == .on ? true : false + self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState) + + if self.boxState { + FindAndToggleNSControlState(self.boxSettingsView, state: .off) + self.boxState = false + self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState) + } + + self.display() + } +} diff --git a/ModuleKit/reader.swift b/ModuleKit/reader.swift index 3595880c..f748b1d6 100644 --- a/ModuleKit/reader.swift +++ b/ModuleKit/reader.swift @@ -62,6 +62,7 @@ open class Reader: ReaderInternal_p { private var nilCallbackCounter: Int = 0 private var ready: Bool = false private var locked: Bool = true + private var initlizalized: Bool = false public var active: Bool = false private var history: [T]? = [] @@ -124,7 +125,7 @@ open class Reader: ReaderInternal_p { open func start() { if self.popup && self.locked { if !self.ready { - DispatchQueue.global().async { + DispatchQueue.global(qos: .background).async { self.read() } } @@ -140,9 +141,12 @@ open class Reader: ReaderInternal_p { self.read() }) } - - DispatchQueue.global().async { - self.read() + + if !self.initlizalized { + DispatchQueue.global(qos: .background).async { + self.read() + } + self.initlizalized = true } self.repeatTask?.start() self.active = true @@ -154,11 +158,10 @@ open class Reader: ReaderInternal_p { } open func stop() { - if let repeater = self.repeatTask { - repeater.removeAllObservers(thenStop: true) - } + self.repeatTask?.removeAllObservers(thenStop: true) self.repeatTask = nil self.active = false + self.initlizalized = false } public func setInterval(_ value: Int) { diff --git a/ModuleKit/widget.swift b/ModuleKit/widget.swift index 94ed59ee..5c690d1c 100644 --- a/ModuleKit/widget.swift +++ b/ModuleKit/widget.swift @@ -58,6 +58,7 @@ public enum widget_t: String { case lineChart = "line_chart" case barChart = "bar_chart" case pieChart = "pie_chart" + case networkChart = "network_chart" case speed = "speed" case battery = "battery" case sensors = "sensors" @@ -85,6 +86,7 @@ open class Widget: NSView, Widget_p { case .lineChart: return "Line chart" case .barChart: return "Bar chart" case .pieChart: return "Pie chart" + case .networkChart: return "Network chart" case .speed: return "Speed" case .battery: return "Battery" case .sensors: return "Text" @@ -146,6 +148,9 @@ func LoadWidget(_ type: widget_t, preview: Bool, name: String, config: NSDiction case .pieChart: widget = PieChart(preview: preview, title: name, config: widgetConfig, store: store) break + case .networkChart: + widget = NetworkChart(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/Net/config.plist b/Modules/Net/config.plist index 7fe6dc05..c511b266 100644 --- a/Modules/Net/config.plist +++ b/Modules/Net/config.plist @@ -20,6 +20,13 @@ D + network_chart + + Default + + Order + 1 + diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index 02cd1e97..e7c117dd 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -132,13 +132,15 @@ public class Network: Module { } private func usageCallback(_ value: Network_Usage?) { - if value == nil { + guard let value = value else { return } - self.popupView.usageCallback(value!) + self.popupView.usageCallback(value) if let widget = self.widget as? SpeedWidget { - widget.setValue(upload: value!.bandwidth.upload, download: value!.bandwidth.download) + widget.setValue(upload: value.bandwidth.upload, download: value.bandwidth.download) + } else if let widget = self.widget as? NetworkChart { + widget.setValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download)) } } } diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index cdf0dc49..5fdff913 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 9A58DEA024B363F300716A9F /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58DE9F24B363F300716A9F /* settings.swift */; }; 9A58DEA424B3647600716A9F /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58DEA324B3647600716A9F /* settings.swift */; }; 9A5AF11B2469CE9B00684737 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5AF11A2469CE9B00684737 /* popup.swift */; }; + 9A65295825B78056005E2DE4 /* NetworkChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65295725B78056005E2DE4 /* NetworkChart.swift */; }; 9A65654A253F20EF0096B607 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A656549253F20EF0096B607 /* settings.swift */; }; 9A656562253F788A0096B607 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A656561253F788A0096B607 /* popup.swift */; }; 9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */; }; @@ -402,6 +403,7 @@ 9A58DEA324B3647600716A9F /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; }; 9A5AF11A2469CE9B00684737 /* popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = popup.swift; sourceTree = ""; }; 9A5F0503256A9135002FF75F /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; + 9A65295725B78056005E2DE4 /* NetworkChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkChart.swift; sourceTree = ""; }; 9A654920244074B500E30B74 /* extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = extensions.swift; sourceTree = ""; }; 9A65492224407EA600E30B74 /* store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = store.swift; sourceTree = ""; }; 9A656549253F20EF0096B607 /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; }; @@ -705,6 +707,7 @@ 9AA64263244B94F300416A33 /* LineChart.swift */, 9A1A7AB924561F0B00A84F7A /* BarChart.swift */, 9A20E6D92575555100AC2302 /* PieChart.swift */, + 9A65295725B78056005E2DE4 /* NetworkChart.swift */, 9A3E17E7247AA8E100449CD1 /* Speed.swift */, 9ABFF911248BF39500C9041A /* Battery.swift */, 9AE29AFD249A82B70071B02D /* Sensors.swift */, @@ -1513,6 +1516,7 @@ 9A1A7ABA24561F0B00A84F7A /* BarChart.swift in Sources */, 9A20E6DA2575555100AC2302 /* PieChart.swift in Sources */, 9A944D55244920690058F32A /* reader.swift in Sources */, + 9A65295825B78056005E2DE4 /* NetworkChart.swift in Sources */, 9A7C61B42440DF810032695D /* Mini.swift in Sources */, 9AE29AFE249A82B70071B02D /* Sensors.swift in Sources */, 9A944D5D24492A8B0058F32A /* popup.swift in Sources */, diff --git a/StatsKit/Charts.swift b/StatsKit/Charts.swift index b40991c3..0a84af27 100644 --- a/StatsKit/Charts.swift +++ b/StatsKit/Charts.swift @@ -110,10 +110,12 @@ public class NetworkChartView: NSView { public var id: String = UUID().uuidString public var base: DataSizeBase = .byte - private var points: [(Double, Double)]? = nil + public var points: [(Double, Double)]? = nil private var colors: [NSColor] = [NSColor.systemRed, NSColor.systemBlue] + private var minMax: Bool = false - public init(frame: NSRect, num: Int) { + public init(frame: NSRect, num: Int, minMax: Bool = true) { + self.minMax = minMax self.points = Array(repeating: (0, 0), count: num) super.init(frame: frame) } @@ -197,21 +199,23 @@ public class NetworkChartView: NSView { context.restoreGState() - let stringAttributes = [ - NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .light), - NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor, - NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle() - ] - let uploadText = Units(bytes: Int64(uploadMax)).getReadableSpeed(base: self.base) - let downloadText = Units(bytes: Int64(downloadMax)).getReadableSpeed(base: self.base) - let uploadTextWidth = uploadText.widthOfString(usingFont: stringAttributes[NSAttributedString.Key.font] as! NSFont) - let downloadTextWidth = downloadText.widthOfString(usingFont: stringAttributes[NSAttributedString.Key.font] as! NSFont) - - var rect = CGRect(x: 1, y: height - 9, width: uploadTextWidth, height: 8) - NSAttributedString.init(string: uploadText, attributes: stringAttributes).draw(with: rect) - - rect = CGRect(x: 1, y: 2, width: downloadTextWidth, height: 8) - NSAttributedString.init(string: downloadText, attributes: stringAttributes).draw(with: rect) + if self.minMax { + let stringAttributes = [ + NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .light), + NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor, + NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle() + ] + let uploadText = Units(bytes: Int64(uploadMax)).getReadableSpeed(base: self.base) + let downloadText = Units(bytes: Int64(downloadMax)).getReadableSpeed(base: self.base) + let uploadTextWidth = uploadText.widthOfString(usingFont: stringAttributes[NSAttributedString.Key.font] as! NSFont) + let downloadTextWidth = downloadText.widthOfString(usingFont: stringAttributes[NSAttributedString.Key.font] as! NSFont) + + var rect = CGRect(x: 1, y: height - 9, width: uploadTextWidth, height: 8) + NSAttributedString.init(string: uploadText, attributes: stringAttributes).draw(with: rect) + + rect = CGRect(x: 1, y: 2, width: downloadTextWidth, height: 8) + NSAttributedString.init(string: downloadText, attributes: stringAttributes).draw(with: rect) + } } public func addValue(upload: Double, download: Double) { diff --git a/StatsKit/updater.swift b/StatsKit/updater.swift index 6a8a82fb..7f6d1f35 100644 --- a/StatsKit/updater.swift +++ b/StatsKit/updater.swift @@ -84,7 +84,12 @@ public class macAppUpdater { } private func fetchLastVersion(completionHandler: @escaping (_ result: [String]?, _ error: Error?) -> Void) { - let task = URLSession.shared.dataTask(with: URL(string: self.url)!) { data, response, error in + guard let url = URL(string: self.url) else { + completionHandler(nil, "wrong url") + return + } + + URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data, error == nil else { return } do { @@ -106,8 +111,7 @@ public class macAppUpdater { } catch let parsingError { completionHandler(nil, parsingError) } - } - task.resume() + }.resume() } public func download(_ url: URL, progressHandler: @escaping (_ progress: Progress) -> Void = {_ in }, doneHandler: @escaping (_ path: String) -> Void = {_ in }) {