diff --git a/Kit/module/module.swift b/Kit/module/module.swift index d0cf6ebc..e9e75957 100644 --- a/Kit/module/module.swift +++ b/Kit/module/module.swift @@ -70,7 +70,7 @@ open class Module: Module_p { public var available: Bool = false public var enabled: Bool = false - public var widgets: [Widget] = [] + public var menuBar: MenuBar public var settings: Settings_p? = nil private var settingsView: Settings_v? = nil @@ -86,6 +86,7 @@ open class Module: Module_p { self.log = NextLog.shared.copy(category: self.config.name) self.settingsView = settings self.popupView = popup + self.menuBar = MenuBar(moduleName: self.config.name) self.available = self.isAvailable() self.enabled = Store.shared.bool(key: "\(self.config.name)_state", defaultValue: self.config.defaultState) @@ -112,7 +113,7 @@ open class Module: Module_p { debug("Module started without widget", log: self.log) } - self.settings = Settings(config: &self.config, widgets: &self.widgets, enabled: self.enabled, moduleSettings: self.settingsView) + self.settings = Settings(config: &self.config, widgets: &self.menuBar.widgets, enabled: self.enabled, moduleSettings: self.settingsView) self.settings?.toggleCallback = { [weak self] in self?.toggleEnabled() } @@ -149,7 +150,7 @@ open class Module: Module_p { $0.stop() $0.terminate() } - self.widgets.forEach{ $0.disable() } + self.menuBar.disable() debug("Module terminated", log: self.log) } @@ -166,7 +167,7 @@ open class Module: Module_p { reader.initStoreValues(title: self.config.name) reader.start() } - self.widgets.forEach{ $0.enable() } + self.menuBar.enable() debug("Module enabled", log: self.log) } @@ -177,7 +178,7 @@ open class Module: Module_p { self.enabled = false Store.shared.set(key: "\(self.config.name)_state", value: false) self.readers.forEach{ $0.stop() } - self.widgets.forEach{ $0.disable() } + self.menuBar.disable() self.popup?.setIsVisible(false) debug("Module disabled", log: self.log) } @@ -199,7 +200,7 @@ open class Module: Module_p { // handler for reader, calls when main reader is ready, and return first value public func readyHandler() { - self.widgets.forEach{ $0.enable() } + self.menuBar.enable() debug("Reader report readiness", log: self.log) } @@ -223,7 +224,7 @@ open class Module: Module_p { config: self.config.widgetsConfig, defaultWidget: self.config.defaultWidget ) { - self.widgets.append(widget) + self.menuBar.append(widget) } } } @@ -302,7 +303,7 @@ open class Module: Module_p { guard let name = notification.userInfo?["module"] as? String, name == self.config.name else { return } - let isEmpty = self.widgets.filter({ $0.isActive }).isEmpty + let isEmpty = self.menuBar.widgets.filter({ $0.isActive }).isEmpty var state = self.enabled if isEmpty && self.enabled { diff --git a/Kit/module/settings.swift b/Kit/module/settings.swift index c70212f8..caad8f49 100644 --- a/Kit/module/settings.swift +++ b/Kit/module/settings.swift @@ -41,6 +41,15 @@ open class Settings: NSStackView, Settings_p { return view }() + private var oneViewState: Bool { + get { + return Store.shared.bool(key: "\(self.config.pointee.name)_oneView", defaultValue: false) + } + set { + Store.shared.set(key: "\(self.config.pointee.name)_oneView", value: newValue) + } + } + init(config: UnsafePointer, widgets: UnsafeMutablePointer<[Widget]>, enabled: Bool, moduleSettings: Settings_v?) { self.config = config self.widgets = widgets.pointee @@ -145,7 +154,7 @@ open class Settings: NSStackView, Settings_p { ) view.spacing = Constants.Settings.margin - view.addArrangedSubview(WidgetSelectorView(widgets: self.widgets, stateCallback: self.loadWidget)) + view.addArrangedSubview(WidgetSelectorView(module: self.config.pointee.name, widgets: self.widgets, stateCallback: self.loadWidget)) view.addArrangedSubview(self.settings()) return view @@ -225,6 +234,26 @@ open class Settings: NSStackView, Settings_p { return } + let container = NSStackView() + container.orientation = .vertical + container.distribution = .gravityAreas + container.translatesAutoresizingMaskIntoConstraints = false + container.edgeInsets = NSEdgeInsets( + top: Constants.Settings.margin, + left: Constants.Settings.margin, + bottom: Constants.Settings.margin, + right: Constants.Settings.margin + ) + container.spacing = Constants.Settings.margin + + container.addArrangedSubview(toggleSettingRow( + title: "\(localizedString("Merge widgets into one"))", + action: #selector(self.toggleOneView), + state: self.oneViewState + )) + + self.widgetSettingsContainer?.addArrangedSubview(container) + for i in 0...list.count - 1 { self.widgetSettingsContainer?.addArrangedSubview(WidgetSettings( title: list[i].type.name(), @@ -233,12 +262,26 @@ open class Settings: NSStackView, Settings_p { )) } } + + @objc private func toggleOneView(_ 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.oneViewState = state! == .on ? true : false + NotificationCenter.default.post(name: .toggleOneView, object: nil, userInfo: ["module": self.config.pointee.name]) + } } class WidgetSelectorView: NSStackView { + private var module: String private var stateCallback: () -> Void = {} - public init(widgets: [Widget], stateCallback: @escaping () -> Void) { + public init(module: String, widgets: [Widget], stateCallback: @escaping () -> Void) { + self.module = module self.stateCallback = stateCallback super.init(frame: NSRect(x: 0, y: 0, width: 0, height: 0)) @@ -378,6 +421,7 @@ class WidgetSelectorView: NSStackView { } else if newIdx >= separatorIdx { view.status(false) } + NotificationCenter.default.post(name: .widgetRearrange, object: nil, userInfo: ["module": self.module]) } view.mouseUp(with: event) diff --git a/Kit/module/widget.swift b/Kit/module/widget.swift index f6005687..5214a8ff 100644 --- a/Kit/module/widget.swift +++ b/Kit/module/widget.swift @@ -137,8 +137,9 @@ extension widget_t: CaseIterable {} public protocol widget_p: NSView { var type: widget_t { get } var title: String { get } + var position: Int { get set } - var widthHandler: ((CGFloat) -> Void)? { get set } + var widthHandler: (() -> Void)? { get set } func setValues(_ values: [value_t]) func settings() -> NSView @@ -147,8 +148,9 @@ public protocol widget_p: NSView { open class WidgetWrapper: NSView, widget_p { public var type: widget_t public var title: String + public var position: Int = -1 - public var widthHandler: ((CGFloat) -> Void)? = nil + public var widthHandler: (() -> Void)? = nil public init(_ type: widget_t, title: String, frame: NSRect) { self.type = type @@ -168,9 +170,8 @@ open class WidgetWrapper: NSView, widget_p { DispatchQueue.main.async { self.setFrameSize(NSSize(width: width, height: self.frame.size.height)) + self.widthHandler?() } - - self.widthHandler?(width) } // MARK: - stubs @@ -184,7 +185,7 @@ public class Widget { public let defaultWidget: widget_t public let module: String public let image: NSImage - public let item: widget_p + public var item: widget_p public var isActive: Bool { get { @@ -199,11 +200,20 @@ public class Widget { } } - private var config: NSDictionary = NSDictionary() - private var menuBarItem: NSStatusItem? = nil + public var toggleCallback: ((widget_t, Bool) -> Void)? = nil + public var sizeCallback: (() -> Void)? = nil + public var log: NextLog { return NextLog.shared.copy(category: self.module) } + public var position: Int { + get { + return Store.shared.int(key: "\(self.module)_\(self.type)_position", defaultValue: 0) + } + set { + Store.shared.set(key: "\(self.module)_\(self.type)_position", value: newValue) + } + } private var list: [widget_t] { get { @@ -215,52 +225,38 @@ public class Widget { } } + private var config: NSDictionary = NSDictionary() + private var menuBarItem: NSStatusItem? = nil + private var originX: CGFloat + public init(_ type: widget_t, defaultWidget: widget_t, module: String, item: widget_p, image: NSImage) { self.type = type self.module = module self.item = item self.defaultWidget = defaultWidget self.image = image + self.originX = item.frame.origin.x - self.item.widthHandler = { [weak self] value in - if let s = self, let item = s.menuBarItem, item.length != value { - item.length = value - if let this = self { - debug("widget \(s.type) change width to \(Double(value).rounded(toPlaces: 2))", log: this.log) - } + self.item.widthHandler = { [weak self] in + self?.sizeCallback?() + if let s = self, let item = s.menuBarItem, let width: CGFloat = self?.item.frame.width, item.length != width { + item.length = width + debug("widget \(s.type) change width to \(Double(width).rounded(toPlaces: 2))", log: s.log) } } + self.item.identifier = NSUserInterfaceItemIdentifier(self.type.rawValue) } // show item in the menu bar public func enable() { - guard self.isActive else { - return - } - - DispatchQueue.main.async(execute: { - self.menuBarItem = NSStatusBar.system.statusItem(withLength: self.item.frame.width) - self.menuBarItem?.autosaveName = "\(self.module)_\(self.type.name())" - self.menuBarItem?.button?.addSubview(self.item) - - if let item = self.menuBarItem, !item.isVisible { - self.menuBarItem?.isVisible = true - } - - self.menuBarItem?.button?.target = self - self.menuBarItem?.button?.action = #selector(self.togglePopup) - self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown]) - }) - + guard self.isActive else { return } + self.toggleCallback?(self.type, true) debug("widget \(self.type.rawValue) enabled", log: self.log) } // remove item from the menu bar public func disable() { - if let item = self.menuBarItem { - NSStatusBar.system.removeStatusItem(item) - } - self.menuBarItem = nil + self.toggleCallback?(self.type, false) debug("widget \(self.type.rawValue) disabled", log: self.log) } @@ -286,6 +282,30 @@ public class Widget { NotificationCenter.default.post(name: .toggleWidget, object: nil, userInfo: ["module": self.module]) } + public func setMenuBarItem(state: Bool) { + if state { + DispatchQueue.main.async(execute: { + self.menuBarItem = NSStatusBar.system.statusItem(withLength: self.item.frame.width) + self.menuBarItem?.autosaveName = "\(self.module)_\(self.type.name())" + if self.item.frame.origin.x != self.originX { + self.item.setFrameOrigin(NSPoint(x: self.originX, y: self.item.frame.origin.y)) + } + self.menuBarItem?.button?.addSubview(self.item) + + if let item = self.menuBarItem, !item.isVisible { + self.menuBarItem?.isVisible = true + } + + self.menuBarItem?.button?.target = self + self.menuBarItem?.button?.action = #selector(self.togglePopup) + self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown]) + }) + } else if let item = self.menuBarItem { + NSStatusBar.system.removeStatusItem(item) + self.menuBarItem = nil + } + } + @objc private func togglePopup(_ sender: Any) { if let item = self.menuBarItem, let window = item.button?.window { NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [ @@ -296,3 +316,175 @@ public class Widget { } } } + +public class MenuBar { + public var widgets: [Widget] = [] + + private var moduleName: String + private var menuBarItem: NSStatusItem? = nil + private var view: MenuBarView = MenuBarView() + + public var oneView: Bool = false + public var activeWidgets: [Widget] { + get { + return self.widgets.filter({ $0.isActive }) + } + } + public var sortedWidgets: [widget_t] { + get { + var list: [widget_t: Int] = [:] + self.activeWidgets.forEach { (w: Widget) in + list[w.type] = w.position + } + return list.sorted { $0.1 < $1.1 }.map{ $0.key } + } + } + + init(moduleName: String) { + self.moduleName = moduleName + self.oneView = Store.shared.bool(key: "\(self.moduleName)_oneView", defaultValue: self.oneView) + self.setupMenuBarItem(self.oneView) + + NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(listenForWidgetRearrange), name: .widgetRearrange, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + public func append(_ widget: Widget) { + widget.toggleCallback = { [weak self] (type, state) in + if let s = self, s.oneView { + if state, let w = s.activeWidgets.first(where: { $0.type == type }) { + DispatchQueue.main.async(execute: { + s.recalculateWidth() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + s.view.addWidget(w.item, position: w.position) + s.view.recalculate(s.sortedWidgets) + } + }) + } else { + DispatchQueue.main.async(execute: { + s.view.removeWidget(type: type) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + s.recalculateWidth() + s.view.recalculate(s.sortedWidgets) + } + }) + } + } else { + widget.setMenuBarItem(state: state) + } + } + widget.sizeCallback = { [weak self] in + self?.recalculateWidth() + } + self.widgets.append(widget) + } + + public func enable() { + self.widgets.forEach{ $0.enable() } + } + + public func disable() { + self.widgets.forEach{ $0.disable() } + } + + private func setupMenuBarItem(_ state: Bool) { + if state { + self.menuBarItem = NSStatusBar.system.statusItem(withLength: 0) + self.menuBarItem?.autosaveName = self.moduleName + self.menuBarItem?.isVisible = true + + self.menuBarItem?.button?.addSubview(self.view) + self.menuBarItem?.button?.target = self + self.menuBarItem?.button?.action = #selector(self.togglePopup) + self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown]) + } else if let item = self.menuBarItem { + NSStatusBar.system.removeStatusItem(item) + self.menuBarItem = nil + } + } + + private func recalculateWidth() { + guard self.oneView else { return } + + let w = self.activeWidgets.map({ $0.item.frame.width }).reduce(0, +) + + (CGFloat(self.activeWidgets.count - 1) * Constants.Widget.spacing) + + Constants.Widget.spacing * 2 + self.menuBarItem?.length = w + self.view.setFrameSize(NSSize(width: w, height: Constants.Widget.height)) + + self.view.recalculate(self.sortedWidgets) + } + + @objc private func togglePopup(_ sender: Any) { + if let item = self.menuBarItem, let window = item.button?.window { + NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [ + "module": self.moduleName, + "origin": window.frame.origin, + "center": window.frame.width/2 + ]) + } + } + + @objc private func listenForOneView(_ notification: Notification) { + guard let name = notification.userInfo?["module"] as? String, name == self.moduleName else { + return + } + + self.activeWidgets.forEach { (w: Widget) in + w.disable() + } + + self.setupMenuBarItem(!self.oneView) + self.recalculateWidth() + + self.oneView = Store.shared.bool(key: "\(self.moduleName)_oneView", defaultValue: self.oneView) + + self.activeWidgets.forEach { (w: Widget) in + w.enable() + } + } + + @objc private func listenForWidgetRearrange(_ notification: Notification) { + guard let name = notification.userInfo?["module"] as? String, name == self.moduleName else { + return + } + + self.view.recalculate(self.sortedWidgets) + } +} + +public class MenuBarView: NSView { + init() { + super.init(frame: NSRect.zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func addWidget(_ view: NSView, position: Int) { + self.addSubview(view) + } + + public func removeWidget(type: widget_t) { + if let view = self.subviews.first(where: { $0.identifier == NSUserInterfaceItemIdentifier(type.rawValue) }) { + view.removeFromSuperview() + } else { + error("\(type) cound not be removed from the one view bacause not found!") + } + } + + public func recalculate(_ list: [widget_t] = []) { + var x: CGFloat = Constants.Widget.spacing + list.forEach { (type: widget_t) in + if let view = self.subviews.first(where: { $0.identifier == NSUserInterfaceItemIdentifier(type.rawValue) }) { + view.setFrameOrigin(NSPoint(x: x, y: view.frame.origin.y)) + x = view.frame.origin.x + view.frame.width + Constants.Widget.spacing + } + } + } +} diff --git a/Kit/types.swift b/Kit/types.swift index 5d7fed39..baaf7f20 100644 --- a/Kit/types.swift +++ b/Kit/types.swift @@ -202,6 +202,8 @@ public extension Notification.Name { static let refreshPublicIP = Notification.Name("refreshPublicIP") static let resetTotalNetworkUsage = Notification.Name("resetTotalNetworkUsage") static let syncFansControl = Notification.Name("syncFansControl") + static let toggleOneView = Notification.Name("toggleOneView") + static let widgetRearrange = Notification.Name("widgetRearrange") } public var isARM: Bool { diff --git a/Modules/Battery/main.swift b/Modules/Battery/main.swift index 277f92c5..334d1633 100644 --- a/Modules/Battery/main.swift +++ b/Modules/Battery/main.swift @@ -125,7 +125,7 @@ public class Battery: Module { self.checkHighNotification(value: value) self.popupView.usageCallback(value) - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as Mini: widget.setValue(abs(value.level)) diff --git a/Modules/Bluetooth/main.swift b/Modules/Bluetooth/main.swift index dbfa599f..24141121 100644 --- a/Modules/Bluetooth/main.swift +++ b/Modules/Bluetooth/main.swift @@ -86,7 +86,7 @@ public class Bluetooth: Module { } } - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as SensorsWidget: widget.setValues(list) default: break diff --git a/Modules/CPU/main.swift b/Modules/CPU/main.swift index 14c84713..02a243c9 100644 --- a/Modules/CPU/main.swift +++ b/Modules/CPU/main.swift @@ -164,7 +164,7 @@ public class CPU: Module { self.popupView.loadCallback(value) self.checkNotificationLevel(value.totalUsage) - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as Mini: widget.setValue(value.totalUsage) case let widget as LineChart: widget.setValue(value.totalUsage) diff --git a/Modules/Disk/main.swift b/Modules/Disk/main.swift index 1fa8b619..81e6e5f3 100644 --- a/Modules/Disk/main.swift +++ b/Modules/Disk/main.swift @@ -219,7 +219,7 @@ public class Disk: Module { self.checkNotificationLevel(percentage) - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as Mini: widget.setValue(percentage) case let widget as BarChart: widget.setValue([[ColorValue(percentage)]]) @@ -246,7 +246,7 @@ public class Disk: Module { return } - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as SpeedWidget: widget.setValue(upload: d.activity.write, download: d.activity.read) case let widget as NetworkChart: widget.setValue(upload: Double(d.activity.write), download: Double(d.activity.read)) diff --git a/Modules/GPU/main.swift b/Modules/GPU/main.swift index 0daafb13..56b0da40 100644 --- a/Modules/GPU/main.swift +++ b/Modules/GPU/main.swift @@ -140,7 +140,7 @@ public class GPU: Module { self.checkNotificationLevel(utilization) - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as Mini: widget.setValue(utilization) diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index b7a9a183..8af0564c 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -178,7 +178,7 @@ public class Network: Module { self.popupView.usageCallback(value) - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as SpeedWidget: widget.setValue(upload: value.bandwidth.upload, download: value.bandwidth.download) case let widget as NetworkChart: widget.setValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download)) diff --git a/Modules/RAM/main.swift b/Modules/RAM/main.swift index 1051a956..2bd42372 100644 --- a/Modules/RAM/main.swift +++ b/Modules/RAM/main.swift @@ -129,7 +129,7 @@ public class RAM: Module { self.checkNotificationLevel(value.usage) let total: Double = value.total == 0 ? 1 : value.total - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as Mini: widget.setValue(value.usage) diff --git a/Modules/Sensors/main.swift b/Modules/Sensors/main.swift index c9fb417c..eee0de56 100644 --- a/Modules/Sensors/main.swift +++ b/Modules/Sensors/main.swift @@ -94,7 +94,7 @@ public class Sensors: Module { self.popupView.usageCallback(value) - self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { case let widget as SensorsWidget: widget.setValues(list) case let widget as BarChart: widget.setValue(flatList) diff --git a/Modules/Sensors/settings.swift b/Modules/Sensors/settings.swift index 955b3033..83259fd8 100644 --- a/Modules/Sensors/settings.swift +++ b/Modules/Sensors/settings.swift @@ -75,7 +75,7 @@ internal class Settings: NSStackView, Settings_v { )) self.addArrangedSubview(toggleSettingRow( - title: localizedString("Synchronize the fans control"), + title: localizedString("Synchronize fan's control"), action: #selector(toggleFansSync), state: self.fansSyncState )) diff --git a/Stats/Views/Settings.swift b/Stats/Views/Settings.swift index 06e38e9e..81fdd470 100644 --- a/Stats/Views/Settings.swift +++ b/Stats/Views/Settings.swift @@ -84,7 +84,7 @@ class SettingsWindow: NSWindow, NSWindowDelegate { public func setModules() { self.viewController.setModules(modules) - if modules.filter({ $0.enabled != false && $0.available != false && !$0.widgets.filter({ $0.isActive }).isEmpty }).isEmpty { + if modules.filter({ $0.enabled != false && $0.available != false && !$0.menuBar.widgets.filter({ $0.isActive }).isEmpty }).isEmpty { self.setIsVisible(true) } }