feat: added option to show all widgets per module as one item in the menu bar. It's an early feature, so there is a bugs. To work fine with this feature a lot of widgets must be adjusted. It supports organizing the elements right from the widget selector settings already. IT'S AN EARLY FEATURE!

This commit is contained in:
Serhiy Mytrovtsiy
2022-07-09 11:37:00 +02:00
parent ab0a997912
commit 0b7be467e1
14 changed files with 295 additions and 56 deletions

View File

@@ -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 {

View File

@@ -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<module_c>, 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)

View File

@@ -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
}
}
}
}

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
))

View File

@@ -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)
}
}