diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index 87bf9c87..63d095e6 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -27,12 +27,17 @@ public struct Network_interface { var address: String = "" } +public struct Network_addr { + var v4: String? = nil + var v6: String? = nil +} + public struct Network_Usage: value_t { var bandwidth: Bandwidth = (0, 0) var total: Bandwidth = (0, 0) var laddr: String? = nil // local ip - var raddr: String? = nil // remote ip + var raddr: Network_addr = Network_addr() // remote ip var interface: Network_interface? = nil var connectionType: Network_t? = nil @@ -44,7 +49,7 @@ public struct Network_Usage: value_t { self.bandwidth = (0, 0) self.laddr = nil - self.raddr = nil + self.raddr = Network_addr() self.interface = nil self.connectionType = nil diff --git a/Modules/Net/popup.swift b/Modules/Net/popup.swift index 5c5ed2e4..2aa79fa5 100644 --- a/Modules/Net/popup.swift +++ b/Modules/Net/popup.swift @@ -13,19 +13,12 @@ import Cocoa import ModuleKit import StatsKit -internal class Popup: NSView, Popup_p { +internal class Popup: NSStackView, Popup_p { + public var sizeCallback: ((NSSize) -> Void)? = nil + private var store: UnsafePointer private var title: String - private var grid: NSGridView? = nil - - private let dashboardHeight: CGFloat = 90 - private let chartHeight: CGFloat = 90 + Constants.Popup.separatorHeight - private let detailsHeight: CGFloat = (22*7) + Constants.Popup.separatorHeight - private let processHeight: CGFloat = 22 - - private var dashboardView: NSView? = nil - private var uploadView: NSView? = nil private var uploadValue: Int64 = 0 private var uploadValueField: NSTextField? = nil @@ -38,7 +31,6 @@ internal class Popup: NSView, Popup_p { private var downloadUnitField: NSTextField? = nil private var downloadStateView: ColorView? = nil - private var publicIPField: ValueField? = nil private var localIPField: ValueField? = nil private var interfaceField: ValueField? = nil private var ssidField: ValueField? = nil @@ -46,6 +38,12 @@ internal class Popup: NSView, Popup_p { private var totalUploadField: ValueField? = nil private var totalDownloadField: ValueField? = nil + private var publicIPStackView: NSStackView? = nil + private var publicIPv4Field: ValueField? = nil + private var publicIPv6Field: ValueField? = nil + + private var processesView: NSView? = nil + private var initialized: Bool = false private var processesInitialized: Bool = false @@ -65,110 +63,90 @@ internal class Popup: NSView, Popup_p { private var processesHeight: CGFloat { get { let num = self.numberOfProcesses - return (self.processHeight*CGFloat(num)) + (num == 0 ? 0 : Constants.Popup.separatorHeight) + return (22*CGFloat(num)) + (num == 0 ? 0 : Constants.Popup.separatorHeight) } } - public var sizeCallback: ((NSSize) -> Void)? = nil - public init(_ title: String, store: UnsafePointer) { - self.store = store self.title = title + self.store = store super.init(frame: NSRect( x: 0, y: 0, width: Constants.Popup.width, - height: self.dashboardHeight + self.chartHeight + self.detailsHeight + height: 0 )) - self.setFrameSize(NSSize(width: self.frame.width, height: self.frame.height+self.processesHeight)) - let gridView: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)) - gridView.rowSpacing = 0 - gridView.yPlacement = .fill + self.spacing = 0 + self.orientation = .vertical - gridView.addRow(with: [self.initDashboard()]) - gridView.addRow(with: [self.initChart()]) - gridView.addRow(with: [self.initDetails()]) - gridView.addRow(with: [self.initProcesses()]) + self.addArrangedSubview(self.initDashboard()) + self.addArrangedSubview(self.initChart()) + self.addArrangedSubview(self.initDetails()) + self.addArrangedSubview(self.initPublicIP()) + self.addArrangedSubview(self.initProcesses()) - gridView.row(at: 0).height = self.dashboardHeight - gridView.row(at: 1).height = self.chartHeight - gridView.row(at: 2).height = self.detailsHeight - - self.addSubview(gridView) - self.grid = gridView + self.recalculateHeight() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public func numberOfProcessesUpdated() { - if self.processes.count == self.numberOfProcesses { - return - } - - DispatchQueue.main.async(execute: { - self.processes = [] - - let h: CGFloat = self.dashboardHeight + self.chartHeight + self.detailsHeight + self.processesHeight + private func recalculateHeight() { + let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +) + if self.frame.size.height != h { self.setFrameSize(NSSize(width: self.frame.width, height: h)) - - self.grid?.setFrameSize(NSSize(width: self.frame.width, height: h)) - - self.grid?.row(at: 3).cell(at: 0).contentView?.removeFromSuperview() - self.grid?.removeRow(at: 3) - self.grid?.addRow(with: [self.initProcesses()]) - self.processesInitialized = false - self.sizeCallback?(self.frame.size) - }) + } } + // MARK: - views + private func initDashboard() -> NSView { - 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: 0, width: view.frame.width, height: self.dashboardHeight)) + let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 90)) + view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true - let leftPart: NSView = NSView(frame: NSRect(x: 0, y: 0, width: container.frame.width / 2, height: container.frame.height)) + let leftPart: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width / 2, height: view.frame.height)) let uploadFields = self.topValueView(leftPart, title: LocalizedString("Uploading"), color: NSColor.systemRed) self.uploadView = uploadFields.0 self.uploadValueField = uploadFields.1 self.uploadUnitField = uploadFields.2 self.uploadStateView = uploadFields.3 - let rightPart: NSView = NSView(frame: NSRect(x: container.frame.width / 2, y: 0, width: container.frame.width / 2, height: container.frame.height)) + let rightPart: NSView = NSView(frame: NSRect(x: view.frame.width / 2, y: 0, width: view.frame.width / 2, height: view.frame.height)) let downloadFields = self.topValueView(rightPart, title: LocalizedString("Downloading"), color: NSColor.systemBlue) self.downloadView = downloadFields.0 self.downloadValueField = downloadFields.1 self.downloadUnitField = downloadFields.2 self.downloadStateView = downloadFields.3 - container.addSubview(leftPart) - container.addSubview(rightPart) - - view.addSubview(container) - self.dashboardView = container + view.addSubview(leftPart) + view.addSubview(rightPart) return view } private func initChart() -> NSView { - let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.chartHeight)) - let separator = SeparatorView(LocalizedString("Usage history"), origin: NSPoint(x: 0, y: self.chartHeight-Constants.Popup.separatorHeight), width: self.frame.width) + let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 90 + Constants.Popup.separatorHeight)) + view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true + + let separator = SeparatorView(LocalizedString("Usage history"), origin: NSPoint(x: 0, y: 90), width: self.frame.width) let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y)) container.wantsLayer = true container.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor container.layer?.cornerRadius = 3 - self.chart = NetworkChartView(frame: NSRect( + let chart = NetworkChartView(frame: NSRect( x: 0, y: 1, width: container.frame.width, height: container.frame.height - 2 ), num: 120) - self.chart?.base = self.base - container.addSubview(self.chart!) + chart.base = self.base + container.addSubview(chart) + self.chart = chart view.addSubview(separator) view.addSubview(container) @@ -177,22 +155,23 @@ internal class Popup: NSView, Popup_p { } private func initDetails() -> NSView { - let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.detailsHeight)) - let separator = SeparatorView(LocalizedString("Details"), origin: NSPoint(x: 0, y: self.detailsHeight-Constants.Popup.separatorHeight), width: self.frame.width) + let height: CGFloat = 22*6 + let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: height + Constants.Popup.separatorHeight)) + view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true + + let separator = SeparatorView(LocalizedString("Details"), origin: NSPoint(x: 0, y: height), width: self.frame.width) let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y)) - self.totalUploadField = PopupWithColorRow(container, color: NSColor.systemRed, n: 6, title: "\(LocalizedString("Total upload")):", value: "") - self.totalDownloadField = PopupWithColorRow(container, color: NSColor.systemBlue, n: 5, title: "\(LocalizedString("Total download")):", value: "") + self.totalUploadField = PopupWithColorRow(container, color: NSColor.systemRed, n: 5, title: "\(LocalizedString("Total upload")):", value: "0") + self.totalDownloadField = PopupWithColorRow(container, color: NSColor.systemBlue, n: 4, title: "\(LocalizedString("Total download")):", value: "0") - self.publicIPField = PopupRow(container, n: 4, title: "\(LocalizedString("Public IP")):", value: "").1 - self.localIPField = PopupRow(container, n: 3, title: "\(LocalizedString("Local IP")):", value: "").1 - self.interfaceField = PopupRow(container, n: 2, title: "\(LocalizedString("Interface")):", value: "").1 - self.ssidField = PopupRow(container, n: 1, title: "\(LocalizedString("Network")):", value: "").1 - self.macAdressField = PopupRow(container, n: 0, title: "\(LocalizedString("Physical address")):", value: "").1 + self.interfaceField = PopupRow(container, n: 3, title: "\(LocalizedString("Interface")):", value: LocalizedString("Unknown")).1 + self.ssidField = PopupRow(container, n: 2, title: "\(LocalizedString("Network")):", value: LocalizedString("Unknown")).1 + self.macAdressField = PopupRow(container, n: 1, title: "\(LocalizedString("Physical address")):", value: LocalizedString("Unknown")).1 + self.localIPField = PopupRow(container, n: 0, title: "\(LocalizedString("Local IP")):", value: LocalizedString("Unknown")).1 - self.publicIPField?.toolTip = LocalizedString("Click to copy public IP address") - self.localIPField?.toolTip = LocalizedString("Click to copy local IP address") - self.macAdressField?.toolTip = LocalizedString("Click to copy mac address") + self.localIPField?.isSelectable = true + self.macAdressField?.isSelectable = true view.addSubview(separator) view.addSubview(container) @@ -200,6 +179,36 @@ internal class Popup: NSView, Popup_p { return view } + private func initPublicIP() -> 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: view.frame.width, height: 0)) + container.orientation = .vertical + container.spacing = 0 + + container.addArrangedSubview(SeparatorView(LocalizedString("Public IP"), origin: NSPoint(x: 0, y: 0), width: self.frame.width)) + self.publicIPv4Field = PopupRow(container, title: "\(LocalizedString("v4")):", value: LocalizedString("Unknown")).1 + self.publicIPv6Field = PopupRow(container, title: "\(LocalizedString("v6")):", value: LocalizedString("Unknown")).1 + + self.publicIPv4Field?.isSelectable = true + if let valueView = self.publicIPv6Field { + valueView.isSelectable = true + valueView.font = NSFont.systemFont(ofSize: 10, weight: .regular) + valueView.setFrameOrigin(NSPoint(x: valueView.frame.origin.x, y: 1)) + } + + 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 + + self.publicIPStackView = container + + return view + } + private func initProcesses() -> NSView { let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.processesHeight)) let separator = SeparatorView(LocalizedString("Top processes"), origin: NSPoint(x: 0, y: self.processesHeight-Constants.Popup.separatorHeight), width: self.frame.width) @@ -214,9 +223,111 @@ internal class Popup: NSView, Popup_p { view.addSubview(separator) view.addSubview(container) + self.processesView = view return view } + // MARK: - callbacks + + public func numberOfProcessesUpdated() { + if self.processes.count == self.numberOfProcesses { + return + } + + DispatchQueue.main.async(execute: { + self.processes = [] + + if let view = self.processesView { + self.removeView(view) + } + self.addArrangedSubview(self.initProcesses()) + self.processesInitialized = false + + self.recalculateHeight() + }) + } + + public func usageCallback(_ value: Network_Usage) { + DispatchQueue.main.async(execute: { + if (self.window?.isVisible ?? false) || !self.initialized { + self.uploadValue = value.bandwidth.upload + self.downloadValue = value.bandwidth.download + self.setUploadDownloadFields() + + self.totalUploadField?.stringValue = Units(bytes: value.total.upload).getReadableMemory() + self.totalDownloadField?.stringValue = Units(bytes: value.total.download).getReadableMemory() + + if let interface = value.interface { + self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName))" + self.macAdressField?.stringValue = interface.address + } else { + self.interfaceField?.stringValue = LocalizedString("Unknown") + self.macAdressField?.stringValue = LocalizedString("Unknown") + } + + if value.connectionType == .wifi { + self.ssidField?.stringValue = value.ssid ?? "Unknown" + } else { + self.ssidField?.stringValue = LocalizedString("Unavailable") + } + + if let view = self.publicIPv4Field, view.stringValue != value.raddr.v4 { + if let addr = value.raddr.v4 { + view.stringValue = (value.countryCode != nil) ? "\(addr) (\(value.countryCode!))" : addr + } else { + view.stringValue = LocalizedString("Unknown") + } + } + if let view = self.publicIPv6Field, view.stringValue != value.raddr.v6 { + if let addr = value.raddr.v6 { + view.stringValue = addr + } else { + view.stringValue = LocalizedString("Unknown") + } + } + + if self.localIPField?.stringValue != value.laddr { + self.localIPField?.stringValue = value.laddr ?? LocalizedString("Unknown") + } + + self.initialized = true + } + + if let chart = self.chart { + if chart.base != self.base { + chart.base = self.base + } + chart.addValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download)) + } + }) + } + + public func processCallback(_ list: [Network_Process]) { + DispatchQueue.main.async(execute: { + if !(self.window?.isVisible ?? false) && self.processesInitialized { + return + } + + if list.count != self.processes.count { + self.processes.forEach { processView in + processView.clear() + } + } + + for i in 0.. (NSView, NSTextField, NSTextField, ColorView) { let topHeight: CGFloat = 30 let titleHeight: CGFloat = 15 @@ -301,96 +412,6 @@ internal class Popup: NSView, Popup_p { self.uploadStateView?.setState(self.uploadValue != 0) self.downloadStateView?.setState(self.downloadValue != 0) } - - public func usageCallback(_ value: Network_Usage) { - DispatchQueue.main.async(execute: { - if (self.window?.isVisible ?? false) || !self.initialized { - self.uploadValue = value.bandwidth.upload - self.downloadValue = value.bandwidth.download - self.setUploadDownloadFields() - - self.totalUploadField?.stringValue = Units(bytes: value.total.upload).getReadableMemory() - self.totalDownloadField?.stringValue = Units(bytes: value.total.download).getReadableMemory() - - if let interface = value.interface { - self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName))" - self.macAdressField?.stringValue = interface.address - } else { - self.interfaceField?.stringValue = LocalizedString("Unknown") - self.macAdressField?.stringValue = LocalizedString("Unknown") - } - - if value.connectionType == .wifi { - self.ssidField?.stringValue = value.ssid ?? "Unknown" - } else { - self.ssidField?.stringValue = LocalizedString("Unavailable") - } - - if self.publicIPField?.stringValue != value.raddr { - if value.raddr == nil { - self.publicIPField?.stringValue = LocalizedString("Unknown") - } else { - if value.countryCode == nil { - self.publicIPField?.stringValue = value.raddr! - } else { - self.publicIPField?.stringValue = "\(value.raddr!) (\(value.countryCode!))" - } - } - } - if self.localIPField?.stringValue != value.laddr { - self.localIPField?.stringValue = value.laddr ?? LocalizedString("Unknown") - } - - self.initialized = true - } - - if let chart = self.chart { - if chart.base != self.base { - chart.base = self.base - } - chart.addValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download)) - } - }) - } - - public func processCallback(_ list: [Network_Process]) { - DispatchQueue.main.async(execute: { - if !(self.window?.isVisible ?? false) && self.processesInitialized { - return - } - - if list.count != self.processes.count { - self.processes.forEach { processView in - processView.clear() - } - } - - for i in 0.. 0 ? String(arr[0]) : self.stringValue - - let pasteboard = NSPasteboard.general - pasteboard.declareTypes([.string], owner: nil) - pasteboard.setString(value, forType: .string) - } } public class NetworkProcessView: NSView { diff --git a/Modules/Net/readers.swift b/Modules/Net/readers.swift index 1a5baccb..0f20d51b 100644 --- a/Modules/Net/readers.swift +++ b/Modules/Net/readers.swift @@ -229,14 +229,23 @@ internal class UsageReader: Reader { } private func getPublicIP() { - let url = URL(string: "https://api.ipify.org") - do { - if let url = url { - self.usage.raddr = try String(contentsOf: url) + if let url = URL(string: "https://api.ipify.org") { + self.usage.raddr.v4 = try String(contentsOf: url) } } catch let error { - os_log(.error, log: log, "get public ip %s", "\(error)") + os_log(.error, log: log, "get public ipv4 %s", "\(error)") + } + + do { + if let url = URL(string: "https://api64.ipify.org") { + let v6 = try String(contentsOf: url) + if self.usage.raddr.v4 != v6 { + self.usage.raddr.v6 = v6 + } + } + } catch let error { + os_log(.error, log: log, "get public ipv6 %s", "\(error)") } } diff --git a/StatsKit/helpers.swift b/StatsKit/helpers.swift index 89c3f55e..867663c5 100644 --- a/StatsKit/helpers.swift +++ b/StatsKit/helpers.swift @@ -316,6 +316,7 @@ public extension NSBezierPath { public func SeparatorView(_ title: String, origin: NSPoint, width: CGFloat) -> NSView { let view: NSView = NSView(frame: NSRect(x: origin.x, y: origin.y, width: width, height: 30)) + view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: (view.frame.height-15)/2, width: view.frame.width, height: 15)) labelView.stringValue = title @@ -328,16 +329,22 @@ public func SeparatorView(_ title: String, origin: NSPoint, width: CGFloat) -> N return view } -public func PopupRow(_ view: NSView, n: CGFloat, title: String, value: String) -> (LabelField, ValueField) { +public func PopupRow(_ view: NSView, n: CGFloat = 0, title: String, value: String) -> (LabelField, ValueField) { let rowView: NSView = NSView(frame: NSRect(x: 0, y: 22*n, width: view.frame.width, height: 22)) - let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .regular)) + 5 + let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .regular)) + 4 let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: (22-15)/2, width: labelWidth, height: 15), title) let valueView: ValueField = ValueField(frame: NSRect(x: labelWidth, y: (22-15)/2, width: rowView.frame.width - labelWidth, height: 16), value) rowView.addSubview(labelView) rowView.addSubview(valueView) - view.addSubview(rowView) + + if let view = view as? NSStackView { + rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true + view.addArrangedSubview(rowView) + } else { + view.addSubview(rowView) + } return (labelView, valueView) }