// // popup.swift // Net // // Created by Serhiy Mytrovtsiy on 24/05/2020. // Using Swift 5.0. // Running on macOS 10.15. // // Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved. // import Cocoa import Kit // swiftlint:disable:next type_body_length internal class Popup: PopupWrapper { private var uploadContainerView: NSView? = nil private var uploadView: NSView? = nil private var uploadValue: Int64 = 0 private var uploadValueField: NSTextField? = nil private var uploadUnitField: NSTextField? = nil private var uploadStateView: ColorView? = nil private var downloadContainerView: NSView? = nil private var downloadView: NSView? = nil private var downloadValue: Int64 = 0 private var downloadValueField: NSTextField? = nil private var downloadUnitField: NSTextField? = nil private var downloadStateView: ColorView? = nil private var downloadColorView: NSView? = nil private var uploadColorView: NSView? = nil private var totalUploadLabel: LabelField? = nil private var totalUploadField: ValueField? = nil private var totalDownloadLabel: LabelField? = nil private var totalDownloadField: ValueField? = nil private var statusField: ValueField? = nil private var connectivityField: ValueField? = nil private var latencyField: ValueField? = nil private var jitterField: ValueField? = nil private var interfaceView: NSStackView? = nil private var interfaceField: ValueField? = nil private var interfaceStatusField: ValueField? = nil private var macAddressField: ValueField? = nil private var ssidField: ValueField? = nil private var standardField: ValueField? = nil private var channelField: ValueField? = nil private var ssidView: NSView? = nil private var interfaceDetailsState: Bool = false private var standardView: NSView? = nil private var channelView: NSView? = nil private var interfaceSpeedView: NSView? = nil private var interfaceSpeedField: ValueField? = nil private var dnsServersView: NSView? = nil private var dnsServersField: ValueField? = nil private var addressView: NSStackView? = nil private var localIPField: ValueField? = nil private var publicIPv4Field: ValueField? = nil private var publicIPv6Field: ValueField? = nil private var publicIPv4View: NSView? = nil private var publicIPv6View: NSView? = nil private var publicIPState: Bool = true private var processesView: NSView? = nil private var processes: ProcessesView? = nil private var chart: NetworkChartView? = nil private var reverseOrderState: Bool = false private var chartHistory: Int = 180 private var chartScale: Scale = .none private var chartFixedScale: Int = 12 private var chartFixedScaleSize: SizeUnit = .MB private var chartPrefSection: PreferencesSection? = nil private var connectivityChart: GridChartView? = nil private var initialized: Bool = false private var processesInitialized: Bool = false private var connectionInitialized: Bool = false private var lastReset: Date = Date() private var latency: [Double] = [] private var jitter: [Double] = [] private var base: DataSizeBase { DataSizeBase(rawValue: Store.shared.string(key: "\(self.title)_base", defaultValue: "byte")) ?? .byte } private var numberOfProcesses: Int { Store.shared.int(key: "\(self.title)_processes", defaultValue: 8) } private var processesHeight: CGFloat { (22*CGFloat(self.numberOfProcesses)) + (self.numberOfProcesses == 0 ? 0 : Constants.Popup.separatorHeight + 22) } private var downloadColorState: SColor = .secondBlue private var downloadColor: NSColor { var value = NSColor.systemBlue if let color = self.downloadColorState.additional as? NSColor { value = color } return value } private var uploadColorState: SColor = .secondRed private var uploadColor: NSColor { var value = NSColor.systemRed if let color = self.uploadColorState.additional as? NSColor { value = color } return value } public init(_ module: ModuleType) { super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0)) self.spacing = 0 self.orientation = .vertical self.downloadColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_downloadColor", defaultValue: self.downloadColorState.key)) self.uploadColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_uploadColor", defaultValue: self.uploadColorState.key)) self.reverseOrderState = Store.shared.bool(key: "\(self.title)_reverseOrder", defaultValue: self.reverseOrderState) self.chartHistory = Store.shared.int(key: "\(self.title)_chartHistory", defaultValue: self.chartHistory) self.chartScale = Scale.fromString(Store.shared.string(key: "\(self.title)_chartScale", defaultValue: self.chartScale.key)) self.chartFixedScale = Store.shared.int(key: "\(self.title)_chartFixedScale", defaultValue: self.chartFixedScale) self.chartFixedScaleSize = SizeUnit.fromString(Store.shared.string(key: "\(self.title)_chartFixedScaleSize", defaultValue: self.chartFixedScaleSize.key)) self.publicIPState = Store.shared.bool(key: "\(self.title)_publicIP", defaultValue: self.publicIPState) self.interfaceDetailsState = Store.shared.bool(key: "\(self.title)_interfaceDetails", defaultValue: self.interfaceDetailsState) self.addArrangedSubview(self.initDashboard()) self.addArrangedSubview(self.initChart()) self.addArrangedSubview(self.initConnectivityChart()) self.addArrangedSubview(self.initDetails()) self.addArrangedSubview(self.initInterface()) self.addArrangedSubview(self.initAddress()) self.addArrangedSubview(self.initProcesses()) if !self.publicIPState { self.addressView?.removeFromSuperview() } self.recalculateHeight() NotificationCenter.default.addObserver(self, selector: #selector(self.resetTotalNetworkUsageCallback), name: .resetTotalNetworkUsage, object: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { NotificationCenter.default.removeObserver(self, name: .resetTotalNetworkUsage, object: nil) } private func recalculateHeight() { var h: CGFloat = 0 self.arrangedSubviews.forEach { v in if let v = v as? NSStackView { h += v.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +) } else { h += v.bounds.height } } if self.frame.size.height != h { self.setFrameSize(NSSize(width: self.frame.width, height: h)) self.sizeCallback?(self.frame.size) } } // MARK: - views private func initDashboard() -> NSView { 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: view.frame.width / 2, height: view.frame.height)) let downloadFields = self.topValueView(leftPart, title: localizedString("Downloading"), color: self.downloadColor) self.downloadContainerView = leftPart self.downloadView = downloadFields.0 self.downloadValueField = downloadFields.1 self.downloadUnitField = downloadFields.2 self.downloadStateView = downloadFields.3 let rightPart: NSView = NSView(frame: NSRect(x: view.frame.width / 2, y: 0, width: view.frame.width / 2, height: view.frame.height)) let uploadFields = self.topValueView(rightPart, title: localizedString("Uploading"), color: self.uploadColor) self.uploadContainerView = rightPart self.uploadView = uploadFields.0 self.uploadValueField = uploadFields.1 self.uploadUnitField = uploadFields.2 self.uploadStateView = uploadFields.3 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: 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 let chart = NetworkChartView( frame: NSRect(x: 0, y: 1, width: container.frame.width, height: container.frame.height - 2), num: self.chartHistory, reversedOrder: self.reverseOrderState, outColor: self.uploadColor, inColor: self.downloadColor, scale: self.chartScale, fixedScale: Double(self.chartFixedScaleSize.toBytes(self.chartFixedScale)) ) chart.base = self.base container.addSubview(chart) self.chart = chart view.addSubview(separator) view.addSubview(container) return view } private func initConnectivityChart() -> NSView { let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 30 + Constants.Popup.separatorHeight)) view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true let separator = separatorView(localizedString("Connectivity history"), origin: NSPoint(x: 0, y: 30), 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 let chart = GridChartView(frame: NSRect(x: 0, y: 1, width: container.frame.width, height: container.frame.height - 2), grid: (30, 3)) container.addSubview(chart) self.connectivityChart = chart view.addSubview(separator) view.addSubview(container) return view } private func initDetails() -> NSView { let view = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0)) view.orientation = .vertical view.spacing = 0 let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Popup.separatorHeight)) row.heightAnchor.constraint(equalToConstant: Constants.Popup.separatorHeight).isActive = true let button = NSButtonWithPadding() button.frame = CGRect(x: view.frame.width - 18, y: 6, width: 18, height: 18) button.bezelStyle = .regularSquare button.isBordered = false button.imageScaling = NSImageScaling.scaleAxesIndependently button.contentTintColor = .lightGray button.action = #selector(self.resetTotalNetworkUsage) button.target = self button.toolTip = localizedString("Reset") button.image = Bundle(for: Module.self).image(forResource: "refresh")! row.addSubview(separatorView(localizedString("Details"), width: self.frame.width)) row.addSubview(button) view.addArrangedSubview(row) let totalUpload = popupWithColorRow(view, color: self.uploadColor, title: "\(localizedString("Total upload")):", value: "0") let totalDownload = popupWithColorRow(view, color: self.downloadColor, title: "\(localizedString("Total download")):", value: "0") self.uploadColorView = totalUpload.0 self.totalUploadLabel = totalUpload.1 self.totalUploadField = totalUpload.2 self.downloadColorView = totalDownload.0 self.totalDownloadLabel = totalDownload.1 self.totalDownloadField = totalDownload.2 self.statusField = popupRow(view, title: "\(localizedString("Status")):", value: localizedString("Unknown")).1 self.connectivityField = popupRow(view, title: "\(localizedString("Internet connection")):", value: localizedString("Unknown")).1 self.latencyField = popupRow(view, title: "\(localizedString("Latency")):", value: "0 ms").1 self.jitterField = popupRow(view, title: "\(localizedString("Jitter")):", value: "0 ms").1 return view } private func initInterface() -> NSView { let view = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0)) view.orientation = .vertical view.spacing = 0 let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Popup.separatorHeight)) row.heightAnchor.constraint(equalToConstant: Constants.Popup.separatorHeight).isActive = true let button = NSButtonWithPadding() button.frame = CGRect(x: view.frame.width - 18, y: 6, width: 18, height: 18) button.bezelStyle = .regularSquare button.isBordered = false button.imageScaling = NSImageScaling.scaleAxesIndependently button.contentTintColor = .lightGray button.action = #selector(self.toggleInterfaceDetails) button.target = self button.toolTip = localizedString("Details") button.image = Bundle(for: Module.self).image(forResource: "tune")! row.addSubview(separatorView(localizedString("Interface"), width: self.frame.width)) row.addSubview(button) view.addArrangedSubview(row) self.interfaceField = popupRow(view, title: "\(localizedString("Interface")):", value: localizedString("Unknown")).1 self.interfaceStatusField = popupRow(view, title: "\(localizedString("Status")):", value: localizedString("Unknown")).1 self.macAddressField = popupRow(view, title: "\(localizedString("Physical address")):", value: localizedString("Unknown")).1 self.macAddressField?.isSelectable = true let ssid = popupRow(view, title: "\(localizedString("Network")):", value: localizedString("Unknown")) let standard = popupRow(view, title: "\(localizedString("Standard")):", value: localizedString("Unavailable")) let channel = popupRow(view, title: "\(localizedString("Channel")):", value: localizedString("Unavailable")) let speed = popupRow(view, title: "\(localizedString("Speed")):", value: localizedString("Unknown")) self.ssidField = ssid.1 self.standardField = standard.1 self.channelField = channel.1 self.interfaceSpeedField = speed.1 self.ssidView = ssid.2 self.standardView = standard.2 self.channelView = channel.2 self.interfaceSpeedView = speed.2 if !self.interfaceDetailsState { self.standardView?.removeFromSuperview() self.channelView?.removeFromSuperview() self.interfaceSpeedView?.removeFromSuperview() } self.interfaceView = view return view } private func initAddress() -> NSView { let view = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0)) view.orientation = .vertical view.spacing = 0 let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Popup.separatorHeight)) row.heightAnchor.constraint(equalToConstant: Constants.Popup.separatorHeight).isActive = true let button = NSButtonWithPadding() button.frame = CGRect(x: view.frame.width - 18, y: 6, width: 18, height: 18) button.bezelStyle = .regularSquare button.isBordered = false button.imageScaling = NSImageScaling.scaleAxesIndependently button.contentTintColor = .lightGray button.action = #selector(self.refreshPublicIP) button.target = self button.toolTip = localizedString("Refresh") button.image = Bundle(for: Module.self).image(forResource: "refresh")! row.addSubview(separatorView(localizedString("Address"), width: self.frame.width)) row.addSubview(button) view.addArrangedSubview(row) self.localIPField = popupRow(view, title: "\(localizedString("Local IP")):", value: localizedString("Unknown")).1 let ipV4 = popupRow(view, title: "\(localizedString("Public IP")):", value: localizedString("Unknown")) let ipV6 = popupRow(view, title: "\(localizedString("Public IP")):", value: localizedString("Unknown")) self.publicIPv4Field = ipV4.1 self.publicIPv6Field = ipV6.1 self.publicIPv4View = ipV4.2 self.publicIPv6View = ipV6.2 self.localIPField?.isSelectable = true self.publicIPv4Field?.isSelectable = true self.publicIPv6Field?.isSelectable = true if let valueView = self.publicIPv6Field { valueView.font = NSFont.systemFont(ofSize: 7, weight: .semibold) valueView.setFrameOrigin(NSPoint(x: valueView.frame.origin.x, y: -1)) } ipV4.2.removeFromSuperview() ipV6.2.removeFromSuperview() self.addressView = view return view } private func initProcesses() -> NSView { if self.numberOfProcesses == 0 { let v = NSView() self.processesView = v return v } 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) let container: ProcessesView = ProcessesView( frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y), values: [(localizedString("Downloading"), self.downloadColor), (localizedString("Uploading"), self.uploadColor)], n: self.numberOfProcesses ) self.processes = container 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.processesView?.removeFromSuperview() self.processesView = nil self.processes = nil 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 { var resized = false 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() let form = DateComponentsFormatter() form.maximumUnitCount = 2 form.unitsStyle = .full form.allowedUnits = [.day, .hour, .minute] if let duration = form.string(from: self.lastReset, to: Date()) { self.totalUploadLabel?.toolTip = localizedString("Last reset", duration) self.totalDownloadLabel?.toolTip = localizedString("Last reset", duration) } if let interface = value.interface { self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName)" if let cc = value.wifiDetails.countryCode { self.interfaceField?.stringValue += ", \(cc)" } self.interfaceField?.stringValue += ")" self.interfaceStatusField?.stringValue = localizedString(interface.status ? "UP" : "DOWN") self.macAddressField?.stringValue = interface.address self.interfaceSpeedField?.stringValue = "\(Int(interface.transmitRate.rounded()))baseT" } else { self.interfaceField?.stringValue = localizedString("Unknown") self.interfaceStatusField?.stringValue = localizedString("Unknown") self.macAddressField?.stringValue = localizedString("Unknown") self.interfaceSpeedField?.stringValue = localizedString("Unknown") } if value.connectionType == .wifi { if let view = self.ssidView, view.superview == nil && value.wifiDetails.ssid != nil { self.interfaceView?.addArrangedSubview(view) resized = true } if self.interfaceDetailsState, let view = self.standardView, view.superview == nil && value.wifiDetails.standard != nil { self.interfaceView?.addArrangedSubview(view) resized = true } if self.interfaceDetailsState, let view = self.channelView, view.superview == nil && value.wifiDetails.channel != nil { self.interfaceView?.addArrangedSubview(view) resized = true } self.ssidField?.stringValue = value.wifiDetails.ssid ?? localizedString("Unknown") if let v = value.wifiDetails.RSSI { self.ssidField?.stringValue += " (\(v))" } self.standardField?.stringValue = value.wifiDetails.standard ?? localizedString("Unknown") self.channelField?.stringValue = value.wifiDetails.channel ?? localizedString("Unknown") var rssi = localizedString("Unknown") if let v = value.wifiDetails.RSSI { rssi = "\(v) dBm" } var noise = localizedString("Unknown") if let v = value.wifiDetails.noise { noise = "\(v) dBm" } let number = value.wifiDetails.channelNumber ?? localizedString("Unknown") let band = value.wifiDetails.channelBand ?? localizedString("Unknown") let width = value.wifiDetails.channelWidth ?? localizedString("Unknown") self.channelField?.toolTip = "RSSI: \(rssi)\nNoise: \(noise)\nChannel number: \(number)\nChannel band: \(band)\nChannel width: \(width)\n" } else { if self.ssidView?.superview != nil { self.ssidField?.stringValue = localizedString("Unavailable") self.ssidView?.removeFromSuperview() resized = true } if self.standardField?.superview != nil { self.standardField?.stringValue = localizedString("Unavailable") self.standardView?.removeFromSuperview() resized = true } if self.channelView?.superview != nil { self.channelField?.stringValue = localizedString("Unavailable") self.channelView?.removeFromSuperview() resized = true } } var privateIP = localizedString("Unknown") if let v4 = value.laddr.v4, !v4.isEmpty { privateIP = v4 } else if let v6 = value.laddr.v6, !v6.isEmpty { privateIP = v6 } if self.localIPField?.stringValue != privateIP { self.localIPField?.stringValue = privateIP } if let view = self.publicIPv4View { if let addr = value.raddr.v4 { if view.superview == nil { self.addressView?.addArrangedSubview(view) resized = true } var ip = addr if let cc = value.raddr.countryCode, !cc.isEmpty { if let flag = countryFlag(cc) { ip += " \(flag)" } else { ip += " (\(cc))" } self.publicIPv4Field?.toolTip = cc } if self.publicIPv4Field?.stringValue != ip { self.publicIPv4Field?.stringValue = ip } } else if view.superview != nil { view.removeFromSuperview() resized = true self.publicIPv4Field?.stringValue = localizedString("Unknown") } } if let view = self.publicIPv6View { if let addr = value.raddr.v6 { if view.superview == nil { self.addressView?.addArrangedSubview(view) resized = true } var ip = addr if let cc = value.raddr.countryCode { if let flag = countryFlag(cc) { ip += " \(flag)" } else { ip += " (\(cc))" } self.publicIPv6Field?.toolTip = cc } if self.publicIPv6Field?.stringValue != ip { self.publicIPv6Field?.stringValue = ip } } else if view.superview != nil { view.removeFromSuperview() resized = true self.publicIPv6Field?.stringValue = localizedString("Unknown") } } if self.interfaceDetailsState { if !value.dns.isEmpty { let servers = value.dns.joined(separator: "\n") if self.dnsServersField == nil || value.dns.count != self.dnsServersField?.stringValue.split(separator: "\n").count { if let view = self.dnsServersView { view.removeFromSuperview() } let view = popupRow(self.interfaceView, title: "\(localizedString("DNS Server")):", value: servers, multiline: true) self.dnsServersField = view.1 self.dnsServersView = view.2 self.dnsServersField?.isSelectable = true } if self.dnsServersField?.stringValue != servers { self.dnsServersField?.stringValue = servers } resized = true } else if let view = self.dnsServersView { view.removeFromSuperview() resized = true } } self.statusField?.stringValue = localizedString(value.status ? "UP" : "DOWN") if resized { self.recalculateHeight() } 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 connectivityCallback(_ value: Network_Connectivity?) { if self.latency.count >= 90 { self.latency.remove(at: 0) } self.latency.append(value?.latency ?? 0) if self.jitter.count >= 90 { self.jitter.remove(at: 0) } self.jitter.append(value?.jitter ?? 0) DispatchQueue.main.async(execute: { if (self.window?.isVisible ?? false) || !self.connectionInitialized { var text = "Unknown" var latency = localizedString("Unknown") var jitter = localizedString("Unknown") if let v = value { text = v.status ? "UP" : "DOWN" if v.status && !self.latency.isEmpty { latency = "\((self.latency.reduce(0, +) / Double(self.latency.count)).rounded(toPlaces: 2)) ms" } if v.status && !self.jitter.isEmpty { jitter = "\((self.jitter.reduce(0, +) / Double(self.jitter.count)).rounded(toPlaces: 2)) ms" } } self.latencyField?.stringValue = latency self.jitterField?.stringValue = jitter self.connectivityField?.stringValue = localizedString(text) self.connectionInitialized = true } if let value, let chart = self.connectivityChart { chart.addValue(value.status) } }) } public func processCallback(_ list: [Network_Process]) { DispatchQueue.main.async(execute: { if !(self.window?.isVisible ?? false) && self.processesInitialized { return } let list = list.map{ $0 } if list.count != self.processes?.count { self.processes?.clear() } for i in 0.. NSView? { let view = SettingsContainerView() view.addArrangedSubview(PreferencesSection([ PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView( callback: self.setKeyboardShortcut, value: self.keyboardShortcut )) ])) view.addArrangedSubview(PreferencesSection([ PreferencesRow(localizedString("Color of download"), component: selectView( action: #selector(self.toggleDownloadColor), items: SColor.allColors, selected: self.downloadColorState.key )), PreferencesRow(localizedString("Color of upload"), component: selectView( action: #selector(self.toggleUploadColor), items: SColor.allColors, selected: self.uploadColorState.key )) ])) view.addArrangedSubview(PreferencesSection([ PreferencesRow(localizedString("Reverse order"), component: switchView( action: #selector(self.toggleReverseOrder), state: self.reverseOrderState )) ])) self.chartPrefSection = PreferencesSection([ PreferencesRow(localizedString("Chart history"), component: selectView( action: #selector(self.togglechartHistory), items: LineChartHistory, selected: "\(self.chartHistory)" )), PreferencesRow(localizedString("Main chart scaling"), component: selectView( action: #selector(self.toggleChartScale), items: Scale.allCases, selected: self.chartScale.key )), PreferencesRow(localizedString("Scale value"), component: StepperInput( self.chartFixedScale, range: NSRange(location: 1, length: 1023), unit: self.chartFixedScaleSize.key, units: SizeUnit.allCases, callback: self.toggleFixedScale, unitCallback: self.toggleFixedScaleSize )) ]) view.addArrangedSubview(self.chartPrefSection!) self.chartPrefSection?.setRowVisibility(2, newState: self.chartScale == .fixed) view.addArrangedSubview(PreferencesSection([ PreferencesRow(localizedString("Public IP"), component: switchView( action: #selector(self.togglePublicIP), state: self.publicIPState )) ])) return view } @objc private func toggleUploadColor(_ sender: NSMenuItem) { guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else { return } self.uploadColorState = newValue Store.shared.set(key: "\(self.title)_uploadColor", value: key) if let color = newValue.additional as? NSColor { self.processes?.setColor(1, color) self.uploadColorView?.layer?.backgroundColor = color.cgColor self.uploadStateView?.setColor(color) self.chart?.setColors(out: color) } } @objc private func toggleDownloadColor(_ sender: NSMenuItem) { guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else { return } self.downloadColorState = newValue Store.shared.set(key: "\(self.title)_downloadColor", value: key) if let color = newValue.additional as? NSColor { self.processes?.setColor(0, color) self.downloadColorView?.layer?.backgroundColor = color.cgColor self.downloadStateView?.setColor(color) self.chart?.setColors(in: color) } } @objc private func toggleReverseOrder(_ sender: NSControl) { self.reverseOrderState = controlState(sender) self.chart?.setReverseOrder(self.reverseOrderState) Store.shared.set(key: "\(self.title)_reverseOrder", value: self.reverseOrderState) self.display() } @objc private func togglechartHistory(_ sender: NSMenuItem) { guard let key = sender.representedObject as? String, let value = Int(key) else { return } self.chartHistory = value Store.shared.set(key: "\(self.title)_chartHistory", value: value) self.chart?.reinit(self.chartHistory) } @objc private func toggleChartScale(_ sender: NSMenuItem) { guard let key = sender.representedObject as? String, let value = Scale.allCases.first(where: { $0.key == key }) else { return } self.chartScale = value self.chart?.setScale(self.chartScale, Double(self.chartFixedScaleSize.toBytes(self.chartFixedScale))) self.chartPrefSection?.setRowVisibility(2, newState: self.chartScale == .fixed) Store.shared.set(key: "\(self.title)_chartScale", value: key) self.display() } @objc private func togglePublicIP(_ sender: NSControl) { self.publicIPState = controlState(sender) Store.shared.set(key: "\(self.title)_publicIP", value: self.publicIPState) DispatchQueue.main.async(execute: { if !self.publicIPState { self.addressView?.removeFromSuperview() } else if let view = self.addressView { self.insertArrangedSubview(view, at: 4) } self.recalculateHeight() }) } @objc private func toggleFixedScale(_ newValue: Int) { self.chart?.setScale(self.chartScale, Double(self.chartFixedScaleSize.toBytes(newValue))) Store.shared.set(key: "\(self.title)_chartFixedScale", value: newValue) } private func toggleFixedScaleSize(_ newValue: KeyValue_p) { guard let newUnit = newValue as? SizeUnit else { return } self.chartFixedScaleSize = newUnit Store.shared.set(key: "\(self.title)_chartFixedScaleSize", value: self.chartFixedScaleSize.key) self.display() } @objc private func toggleInterfaceDetails() { self.interfaceDetailsState = !self.interfaceDetailsState Store.shared.set(key: "\(self.title)_interfaceDetails", value: self.interfaceDetailsState) if !self.interfaceDetailsState { self.standardView?.removeFromSuperview() self.channelView?.removeFromSuperview() self.interfaceSpeedView?.removeFromSuperview() self.dnsServersView?.removeFromSuperview() } else { if let view = self.standardView, view.superview == nil && self.standardField?.stringValue != localizedString("Unavailable") { self.interfaceView?.addArrangedSubview(view) } if let view = self.channelView, view.superview == nil && self.channelField?.stringValue != localizedString("Unavailable") { self.interfaceView?.addArrangedSubview(view) } if let view = self.interfaceSpeedView, view.superview == nil { self.interfaceView?.addArrangedSubview(view) } if let view = self.dnsServersView, view.superview == nil { self.interfaceView?.addArrangedSubview(view) } } self.recalculateHeight() } // MARK: - helpers private func topValueView(_ view: NSView, title: String, color: NSColor) -> (NSView, NSTextField, NSTextField, ColorView) { let topHeight: CGFloat = 30 let titleHeight: CGFloat = 15 view.setAccessibilityElement(true) view.toolTip = title let valueWidth = "0".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5 let unitWidth = "KB/s".widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5 let topPartWidth = valueWidth + unitWidth let topView: NSView = NSView(frame: NSRect( x: (view.frame.width-topPartWidth)/2, y: (view.frame.height - topHeight - titleHeight)/2 + titleHeight, width: topPartWidth, height: topHeight )) let valueField = LabelField(frame: NSRect(x: 0, y: 0, width: valueWidth, height: 30), "0") valueField.font = NSFont.systemFont(ofSize: 26, weight: .light) valueField.textColor = .textColor valueField.alignment = .right let unitField = LabelField(frame: NSRect(x: valueField.frame.width, y: 4, width: unitWidth, height: 15), "KB/s") unitField.font = NSFont.systemFont(ofSize: 13, weight: .light) unitField.textColor = .labelColor unitField.alignment = .left let titleWidth: CGFloat = title.widthOfString(usingFont: NSFont.systemFont(ofSize: 12, weight: .regular))+8 let iconSize: CGFloat = 12 let bottomWidth: CGFloat = titleWidth+iconSize let bottomView: NSView = NSView(frame: NSRect( x: (view.frame.width-bottomWidth)/2, y: topView.frame.origin.y - titleHeight, width: bottomWidth, height: titleHeight )) let colorBlock: ColorView = ColorView(frame: NSRect(x: 0, y: 1, width: iconSize, height: iconSize), color: color, radius: 4) let titleField = LabelField(frame: NSRect(x: iconSize, y: 0, width: titleWidth, height: titleHeight), title) titleField.alignment = .center topView.addSubview(valueField) topView.addSubview(unitField) bottomView.addSubview(colorBlock) bottomView.addSubview(titleField) view.addSubview(topView) view.addSubview(bottomView) return (topView, valueField, unitField, colorBlock) } private func setUploadDownloadFields() { let upload = Units(bytes: self.uploadValue).getReadableTuple(base: self.base) let download = Units(bytes: self.downloadValue).getReadableTuple(base: self.base) self.uploadContainerView?.toolTip = "\(localizedString("Uploading")): \(upload.0)\(upload.1)" self.downloadContainerView?.toolTip = "\(localizedString("Downloading")): \(download.0)\(download.1)" var valueWidth = "\(upload.0)".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5 var unitWidth = upload.1.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5 var topPartWidth = valueWidth + unitWidth self.uploadView?.setFrameSize(NSSize(width: topPartWidth, height: self.uploadView!.frame.height)) self.uploadView?.setFrameOrigin(NSPoint(x: ((self.frame.width/2)-topPartWidth)/2, y: self.uploadView!.frame.origin.y)) self.uploadValueField?.setFrameSize(NSSize(width: valueWidth, height: self.uploadValueField!.frame.height)) self.uploadValueField?.stringValue = "\(upload.0)" self.uploadUnitField?.setFrameSize(NSSize(width: unitWidth, height: self.uploadUnitField!.frame.height)) self.uploadUnitField?.setFrameOrigin(NSPoint(x: self.uploadValueField!.frame.width, y: self.uploadUnitField!.frame.origin.y)) self.uploadUnitField?.stringValue = upload.1 valueWidth = "\(download.0)".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5 unitWidth = download.1.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5 topPartWidth = valueWidth + unitWidth self.downloadView?.setFrameSize(NSSize(width: topPartWidth, height: self.downloadView!.frame.height)) self.downloadView?.setFrameOrigin(NSPoint(x: ((self.frame.width/2)-topPartWidth)/2, y: self.downloadView!.frame.origin.y)) self.downloadValueField?.setFrameSize(NSSize(width: valueWidth, height: self.downloadValueField!.frame.height)) self.downloadValueField?.stringValue = "\(download.0)" self.downloadUnitField?.setFrameSize(NSSize(width: unitWidth, height: self.downloadUnitField!.frame.height)) self.downloadUnitField?.setFrameOrigin(NSPoint(x: self.downloadValueField!.frame.width, y: self.downloadUnitField!.frame.origin.y)) self.downloadUnitField?.stringValue = download.1 self.uploadStateView?.setState(self.uploadValue != 0) self.downloadStateView?.setState(self.downloadValue != 0) } @objc private func refreshPublicIP() { NotificationCenter.default.post(name: .refreshPublicIP, object: nil, userInfo: nil) self.localIPField?.stringValue = localizedString("Updating...") self.publicIPv4Field?.stringValue = localizedString("Updating...") self.publicIPv6Field?.stringValue = localizedString("Updating...") } @objc private func resetTotalNetworkUsage() { NotificationCenter.default.post(name: .resetTotalNetworkUsage, object: nil, userInfo: nil) self.totalUploadField?.stringValue = Units(bytes: 0).getReadableMemory() self.totalDownloadField?.stringValue = Units(bytes: 0).getReadableMemory() self.lastReset = Date() } @objc private func resetTotalNetworkUsageCallback() { self.lastReset = Date() } }