diff --git a/Modules/Battery/main.swift b/Modules/Battery/main.swift index aa8f9ec9..8b608d5f 100644 --- a/Modules/Battery/main.swift +++ b/Modules/Battery/main.swift @@ -46,10 +46,12 @@ struct Battery_Usage: value_t { } public class Battery: Module { + private let popupView: Popup + private let settingsView: Settings + private let portalView: Portal + private var usageReader: UsageReader? = nil private var processReader: ProcessReader? = nil - private let popupView: Popup - private var settingsView: Settings private var lowLevelNotificationState: Bool = false private var highLevelNotificationState: Bool = false @@ -58,10 +60,12 @@ public class Battery: Module { public init() { self.settingsView = Settings("Battery") self.popupView = Popup("Battery") + self.portalView = Portal("Battery") super.init( popup: self.popupView, - settings: self.settingsView + settings: self.settingsView, + portal: self.portalView ) guard self.available else { return } @@ -123,6 +127,7 @@ public class Battery: Module { self.checkLowNotification(value: value) self.checkHighNotification(value: value) self.popupView.usageCallback(value) + self.portalView.loadCallback(value) self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { diff --git a/Modules/Battery/popup.swift b/Modules/Battery/popup.swift index f4c94dba..32ae8bb7 100644 --- a/Modules/Battery/popup.swift +++ b/Modules/Battery/popup.swift @@ -57,10 +57,10 @@ internal class Popup: NSView, Popup_p { private var processes: [ProcessView] = [] private var processesInitialized: Bool = false + private var colorState: Bool = false + private var numberOfProcesses: Int { - get { - return Store.shared.int(key: "\(self.title)_processes", defaultValue: 8) - } + return Store.shared.int(key: "\(self.title)_processes", defaultValue: 8) } private var processesHeight: CGFloat { get { @@ -69,9 +69,7 @@ internal class Popup: NSView, Popup_p { } } private var timeFormat: String { - get { - return Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: "short") - } + Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: "short") } public var sizeCallback: ((NSSize) -> Void)? = nil @@ -87,6 +85,8 @@ internal class Popup: NSView, Popup_p { )) self.setFrameSize(NSSize(width: self.frame.width, height: self.frame.height + self.detailsHeight + self.processesHeight)) + self.colorState = Store.shared.bool(key: "\(self.title)_color", defaultValue: self.colorState) + let gridView: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)) gridView.rowSpacing = 0 gridView.yPlacement = .fill @@ -347,14 +347,32 @@ internal class Popup: NSView, Popup_p { // MARK: - Settings public func settings() -> NSView? { - return nil + let view = SettingsContainerView() + + view.addArrangedSubview(toggleSettingRow( + title: localizedString("Colorize battery"), + action: #selector(toggleColor), + state: self.colorState + )) + + return view + } + + @objc private func toggleColor(_ sender: NSControl) { + self.colorState = controlState(sender) + Store.shared.set(key: "\(self.title)_color", value: self.colorState) + self.dashboardBatteryView?.display() } } -private class BatteryView: NSView { +internal class BatteryView: NSView { private var percentage: Double = 0 - public override init(frame: NSRect) { + private var colorState: Bool { + return Store.shared.bool(key: "Battery_color", defaultValue: false) + } + + public override init(frame: NSRect = NSRect.zero) { super.init(frame: frame) } @@ -365,27 +383,41 @@ private class BatteryView: NSView { public override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) - let w: CGFloat = 130 - let h: CGFloat = 50 + guard let ctx = NSGraphicsContext.current?.cgContext else { return } + + let w: CGFloat = min(dirtyRect.width, 120) + let h: CGFloat = min(dirtyRect.height, 50) let x: CGFloat = (dirtyRect.width - w)/2 let y: CGFloat = (dirtyRect.size.height - h) / 2 - let radius: CGFloat = 3 - let batteryFrame = NSBezierPath(roundedRect: NSRect(x: x+1, y: y, width: w, height: h), xRadius: radius, yRadius: radius) + let batteryFrame = NSBezierPath(roundedRect: NSRect(x: x+1, y: y+1, width: w-8, height: h-2), xRadius: 3, yRadius: 3) + NSColor.textColor.set() - let bPX: CGFloat = x+w+1 - let bPY: CGFloat = (dirtyRect.size.height / 2) - 4 - let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX, y: bPY, width: 4, height: 8), xRadius: radius, yRadius: radius) - batteryPoint.lineWidth = 1.1 - batteryPoint.stroke() + let bPX: CGFloat = batteryFrame.bounds.origin.x + batteryFrame.bounds.width + let bPY: CGFloat = batteryFrame.bounds.origin.y + (batteryFrame.bounds.height/2) - 4 + let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX-2, y: bPY, width: 8, height: 8), xRadius: 4, yRadius: 4) batteryPoint.fill() + let batteryPointSeparator = NSBezierPath() + batteryPointSeparator.move(to: CGPoint(x: bPX, y: batteryFrame.bounds.origin.y)) + batteryPointSeparator.line(to: CGPoint(x: bPX, y: batteryFrame.bounds.origin.y + batteryFrame.bounds.height)) + ctx.saveGState() + ctx.setBlendMode(.destinationOut) + NSColor.textColor.set() + batteryPointSeparator.lineWidth = 4 + batteryPointSeparator.stroke() + ctx.restoreGState() + batteryFrame.lineWidth = 1 batteryFrame.stroke() - let maxWidth = w-2 - let inner = NSBezierPath(roundedRect: NSRect(x: x+2, y: y+1, width: maxWidth * CGFloat(self.percentage), height: h-2), xRadius: radius, yRadius: radius) - self.percentage.batteryColor(color: true).set() + let inner = NSBezierPath(roundedRect: NSRect( + x: x+2, + y: y+2, + width: (w-10) * CGFloat(self.percentage), + height: h-4 + ), xRadius: 3, yRadius: 3) + self.percentage.batteryColor(color: self.colorState).set() inner.lineWidth = 0 inner.stroke() inner.close() diff --git a/Modules/Battery/portal.swift b/Modules/Battery/portal.swift new file mode 100644 index 00000000..87d97c08 --- /dev/null +++ b/Modules/Battery/portal.swift @@ -0,0 +1,89 @@ +// +// portal.swift +// Battery +// +// Created by Serhiy Mytrovtsiy on 16/03/2023 +// Using Swift 5.0 +// Running on macOS 13.2 +// +// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import Kit + +internal class Portal: NSStackView, Portal_p { + var name: String + + private let batteryView: BatteryView = BatteryView() + private var levelField: NSTextField = ValueField(frame: NSRect.zero, "") + private var timeField: NSTextField = ValueField(frame: NSRect.zero, "") + + private var initialized: Bool = false + + private var timeFormat: String { + Store.shared.string(key: "\(self.name)_timeFormat", defaultValue: "short") + } + + init(_ name: String) { + self.name = name + + super.init(frame: NSRect.zero) + + self.wantsLayer = true + self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor + self.layer?.cornerRadius = 3 + + self.orientation = .vertical + self.distribution = .fillEqually + self.edgeInsets = NSEdgeInsets( + top: Constants.Popup.margins, + left: Constants.Popup.margins, + bottom: Constants.Popup.margins, + right: Constants.Popup.margins + ) + self.spacing = 0 + + let box: NSStackView = NSStackView() + box.heightAnchor.constraint(equalToConstant: 13).isActive = true + box.orientation = .horizontal + box.spacing = 0 + + self.levelField.font = NSFont.systemFont(ofSize: 12, weight: .medium) + self.timeField.font = NSFont.systemFont(ofSize: 12, weight: .medium) + + box.addArrangedSubview(self.levelField) + box.addArrangedSubview(NSView()) + box.addArrangedSubview(self.timeField) + + self.addArrangedSubview(self.batteryView) + self.addArrangedSubview(box) + + self.heightAnchor.constraint(equalToConstant: Constants.Popup.portalHeight).isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func updateLayer() { + self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor + } + + public func loadCallback(_ value: Battery_Usage) { + DispatchQueue.main.async(execute: { + if (self.window?.isVisible ?? false) || !self.initialized { + self.levelField.stringValue = "\(Int(abs(value.level) * 100))%" + + var seconds: Double = 0 + if value.timeToEmpty != -1 && value.timeToEmpty != 0 { + seconds = Double((value.powerSource == "Battery Power" ? value.timeToEmpty : value.timeToCharge)*60) + } + self.timeField.stringValue = seconds != 0 ? seconds.printSecondsToHoursMinutesSeconds(short: self.timeFormat == "short") : "" + + self.batteryView.setValue(abs(value.level)) + self.initialized = true + } + }) + } +} diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index cac6957f..9e87d5fe 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 5CFE493D2926513E000F2856 /* eu.exelban.Stats.SMC.Helper in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5CFE492729264DF1000F2856 /* eu.exelban.Stats.SMC.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 5CFE494229265418000F2856 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494129265418000F2856 /* uninstall.sh */; }; 5CFE494429265421000F2856 /* changelog.py in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494329265421000F2856 /* changelog.py */; }; + 5EE8037F29C36BDD0063D37D /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE8037E29C36BDD0063D37D /* portal.swift */; }; 9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; }; 9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; }; 9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -367,6 +368,7 @@ 5CFE493B292650F8000F2856 /* Launchd.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Launchd.plist; sourceTree = ""; }; 5CFE494129265418000F2856 /* uninstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = uninstall.sh; sourceTree = ""; }; 5CFE494329265421000F2856 /* changelog.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = changelog.py; sourceTree = ""; }; + 5EE8037E29C36BDD0063D37D /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; }; 62BA5F74254810C8009D0AC2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 63A07F97275018DF00352C46 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; 7A19DAE52552C326001B192F /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -916,6 +918,7 @@ children = ( 9ABFF902248BEBD700C9041A /* main.swift */, 9ABFF90F248BEE7200C9041A /* readers.swift */, + 5EE8037E29C36BDD0063D37D /* portal.swift */, 9ABFF913248C30A800C9041A /* popup.swift */, 9AD64FA124BF86C100419D59 /* settings.swift */, 9ABFF8F9248BEBCB00C9041A /* Info.plist */, @@ -1720,6 +1723,7 @@ files = ( 9ABFF910248BEE7200C9041A /* readers.swift in Sources */, 9ABFF914248C30A800C9041A /* popup.swift in Sources */, + 5EE8037F29C36BDD0063D37D /* portal.swift in Sources */, 9AD64FA224BF86C100419D59 /* settings.swift in Sources */, 9ABFF903248BEBD700C9041A /* main.swift in Sources */, );