mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-15 00:34:08 +09:00
feat: initialized Bluetooth module (#277)
This commit is contained in:
24
Modules/Bluetooth/Info.plist
Normal file
24
Modules/Bluetooth/Info.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
49
Modules/Bluetooth/config.plist
Normal file
49
Modules/Bluetooth/config.plist
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Name</key>
|
||||
<string>Bluetooth</string>
|
||||
<key>State</key>
|
||||
<true/>
|
||||
<key>Widgets</key>
|
||||
<dict>
|
||||
<key>label</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
<key>Title</key>
|
||||
<string>BLE</string>
|
||||
<key>Order</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>mini</key>
|
||||
<dict>
|
||||
<key>Title</key>
|
||||
<string>BLE</string>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
<key>Preview</key>
|
||||
<dict>
|
||||
<key>Title</key>
|
||||
<string>BLE</string>
|
||||
<key>Value</key>
|
||||
<string>0.98</string>
|
||||
</dict>
|
||||
<key>Unsupported colors</key>
|
||||
<array>
|
||||
<string>pressure</string>
|
||||
</array>
|
||||
<key>Order</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>battery</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<true/>
|
||||
<key>Order</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
116
Modules/Bluetooth/main.swift
Normal file
116
Modules/Bluetooth/main.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// main.swift
|
||||
// Bluetooth
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 08/06/2021.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Kit
|
||||
import CoreBluetooth
|
||||
|
||||
public enum BLEType: String {
|
||||
case iPhone
|
||||
case airPods
|
||||
case unknown
|
||||
}
|
||||
|
||||
public struct BLEDevice {
|
||||
let uuid: UUID
|
||||
let name: String
|
||||
let type: BLEType
|
||||
|
||||
var RSSI: Int?
|
||||
var batteryLevel: [KeyValue_t]
|
||||
|
||||
var isConnected: Bool
|
||||
var isPaired: Bool
|
||||
var isInitialized: Bool
|
||||
|
||||
var peripheral: CBPeripheral?
|
||||
}
|
||||
|
||||
public class Bluetooth: Module {
|
||||
private var devicesReader: DevicesReader? = nil
|
||||
private let popupView: Popup = Popup()
|
||||
private let settingsView: Settings
|
||||
|
||||
private var selectedBattery: String = ""
|
||||
|
||||
public init() {
|
||||
self.settingsView = Settings("Bluetooth")
|
||||
|
||||
super.init(
|
||||
popup: self.popupView,
|
||||
settings: self.settingsView
|
||||
)
|
||||
guard self.available else { return }
|
||||
|
||||
self.devicesReader = DevicesReader()
|
||||
self.selectedBattery = Store.shared.string(key: "\(self.config.name)_battery", defaultValue: self.selectedBattery)
|
||||
|
||||
self.settingsView.selectedBatteryHandler = { [unowned self] value in
|
||||
self.selectedBattery = value
|
||||
}
|
||||
|
||||
self.devicesReader?.callbackHandler = { [unowned self] value in
|
||||
self.batteryCallback(value)
|
||||
}
|
||||
self.devicesReader?.readyCallback = { [unowned self] in
|
||||
self.readyHandler()
|
||||
}
|
||||
|
||||
if let reader = self.devicesReader {
|
||||
self.addReader(reader)
|
||||
}
|
||||
}
|
||||
|
||||
private func batteryCallback(_ raw: [BLEDevice]?) {
|
||||
guard let value = raw else {
|
||||
return
|
||||
}
|
||||
|
||||
let active = value.filter{ $0.isPaired && ($0.isConnected || !$0.batteryLevel.isEmpty) }
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.popupView.batteryCallback(active)
|
||||
})
|
||||
self.settingsView.setList(active)
|
||||
|
||||
var battery = active.first?.batteryLevel.first
|
||||
if self.selectedBattery != "" {
|
||||
let pair = self.selectedBattery.split(separator: "@")
|
||||
|
||||
guard let device = value.first(where: { $0.name == pair.first! }) else {
|
||||
error("cannot find selected battery: \(self.selectedBattery)")
|
||||
return
|
||||
}
|
||||
|
||||
if pair.count == 1 {
|
||||
battery = device.batteryLevel.first
|
||||
} else if pair.count == 2 {
|
||||
battery = device.batteryLevel.first{ $0.key == pair.last! }
|
||||
}
|
||||
}
|
||||
|
||||
self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in
|
||||
switch w.item {
|
||||
case let widget as Mini:
|
||||
guard let percentage = Double(battery?.value ?? "0") else {
|
||||
return
|
||||
}
|
||||
widget.setValue(percentage/100)
|
||||
case let widget as BatterykWidget:
|
||||
var percentage: Double? = nil
|
||||
if let value = battery?.value {
|
||||
percentage = (Double(value) ?? 0) / 100
|
||||
}
|
||||
widget.setValue(percentage: percentage)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Modules/Bluetooth/popup.swift
Normal file
111
Modules/Bluetooth/popup.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// popup.swift
|
||||
// Bluetooth
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 22/06/2021.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Kit
|
||||
|
||||
internal class Popup: NSStackView, Popup_p {
|
||||
public var sizeCallback: ((NSSize) -> Void)? = nil
|
||||
|
||||
private var list: [UUID: BLEView] = [:]
|
||||
|
||||
public init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
|
||||
|
||||
self.orientation = .vertical
|
||||
self.spacing = Constants.Popup.margins
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
internal func batteryCallback(_ list: [BLEDevice]) {
|
||||
let views = self.subviews.filter{ $0 is BLEView }.map{ $0 as! BLEView }
|
||||
|
||||
list.reversed().forEach { (ble: BLEDevice) in
|
||||
if let view = views.first(where: { $0.uuid == ble.uuid }) {
|
||||
view.update(ble.batteryLevel)
|
||||
} else {
|
||||
self.addArrangedSubview(BLEView(
|
||||
width: self.frame.width,
|
||||
uuid: ble.uuid,
|
||||
name: ble.name,
|
||||
batteryLevel: ble.batteryLevel
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
|
||||
if h > 0 && self.frame.size.height != h {
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: h))
|
||||
self.sizeCallback?(self.frame.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class BLEView: NSStackView {
|
||||
public var uuid: UUID
|
||||
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: self.bounds.width, height: self.bounds.height)
|
||||
}
|
||||
|
||||
public init(width: CGFloat, uuid: UUID, name: String, batteryLevel: [KeyValue_t]) {
|
||||
self.uuid = uuid
|
||||
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 30))
|
||||
|
||||
self.orientation = .horizontal
|
||||
self.alignment = .centerY
|
||||
self.spacing = 0
|
||||
self.wantsLayer = true
|
||||
self.layer?.cornerRadius = 2
|
||||
|
||||
let nameView: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 16))
|
||||
nameView.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
nameView.stringValue = name
|
||||
|
||||
self.addArrangedSubview(nameView)
|
||||
self.addArrangedSubview(NSView())
|
||||
|
||||
batteryLevel.forEach { (pair: KeyValue_t) in
|
||||
self.addLevel(pair)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func updateLayer() {
|
||||
self.layer?.backgroundColor = isDarkMode ? NSColor(hexString: "#111111", alpha: 0.25).cgColor : NSColor(hexString: "#f5f5f5", alpha: 1).cgColor
|
||||
}
|
||||
|
||||
public func update(_ batteryLevel: [KeyValue_t]) {
|
||||
batteryLevel.forEach { (pair: KeyValue_t) in
|
||||
if let view = self.subviews.first(where: { $0.identifier?.rawValue == pair.key }) as? NSTextField {
|
||||
view.stringValue = "\(pair.value)%"
|
||||
} else {
|
||||
self.addLevel(pair)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addLevel(_ pair: KeyValue_t) {
|
||||
let valueView: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 13))
|
||||
valueView.identifier = NSUserInterfaceItemIdentifier(rawValue: pair.key)
|
||||
valueView.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
||||
valueView.stringValue = "\(pair.value)%"
|
||||
valueView.toolTip = pair.key
|
||||
self.addArrangedSubview(valueView)
|
||||
}
|
||||
}
|
||||
179
Modules/Bluetooth/readers.swift
Normal file
179
Modules/Bluetooth/readers.swift
Normal file
@@ -0,0 +1,179 @@
|
||||
//
|
||||
// readers.swift
|
||||
// Bluetooth
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 08/06/2021.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Kit
|
||||
import CoreBluetooth
|
||||
import IOBluetooth
|
||||
|
||||
internal class DevicesReader: Reader<[BLEDevice]> {
|
||||
private let ble: BluetoothDelegate = BluetoothDelegate()
|
||||
|
||||
init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
self.ble.read()
|
||||
self.callback(self.ble.devices)
|
||||
}
|
||||
}
|
||||
|
||||
class BluetoothDelegate: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||
private var manager: CBCentralManager!
|
||||
private let cache = UserDefaults(suiteName: "/Library/Preferences/com.apple.Bluetooth")
|
||||
|
||||
private var peripherals: [CBPeripheral] = []
|
||||
public var devices: [BLEDevice] = []
|
||||
private var characteristicsDict: [UUID: CBCharacteristic] = [:]
|
||||
|
||||
private let batteryServiceUUID = CBUUID(string: "0x180F")
|
||||
private let batteryCharacteristicsUUID = CBUUID(string: "0x2A19")
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
self.manager = CBCentralManager.init(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
public func read() {
|
||||
IOBluetoothDevice.pairedDevices().forEach { (d) in
|
||||
guard let device = d as? IOBluetoothDevice,
|
||||
let cache = self.findInCache(address: device.addressString) else {
|
||||
return
|
||||
}
|
||||
|
||||
let rssi = device.rawRSSI() == 127 ? nil : Int(device.rawRSSI())
|
||||
|
||||
if let idx = self.devices.firstIndex(where: { $0.uuid == cache.uuid }) {
|
||||
self.devices[idx].isConnected = device.isConnected()
|
||||
self.devices[idx].isPaired = device.isPaired()
|
||||
self.devices[idx].RSSI = rssi
|
||||
} else {
|
||||
self.devices.append(BLEDevice(
|
||||
uuid: cache.uuid,
|
||||
name: device.nameOrAddress,
|
||||
type: .unknown,
|
||||
RSSI: rssi,
|
||||
batteryLevel: cache.batteryLevel,
|
||||
isConnected: device.isConnected(),
|
||||
isPaired: device.isPaired(),
|
||||
isInitialized: false
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func findInCache(address: String) -> (uuid: UUID, batteryLevel: [KeyValue_t])? {
|
||||
guard let plist = self.cache,
|
||||
let deviceCache = plist.object(forKey: "DeviceCache") as? [String: [String: Any]],
|
||||
let coreCache = plist.object(forKey: "CoreBluetoothCache") as? [String: [String: Any]] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let uuid = coreCache.compactMap({ (key, dict) -> UUID? in
|
||||
guard let field = dict.first(where: { $0.key == "DeviceAddress" }),
|
||||
let value = field.value as? String,
|
||||
value == address else {
|
||||
return nil
|
||||
}
|
||||
return UUID(uuidString: key)
|
||||
}).first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var batteryLevel: [KeyValue_t] = []
|
||||
if let d = deviceCache.first(where: { $0.key == address }) {
|
||||
d.value.forEach { (key, value) in
|
||||
guard let value = value as? Int, key == "BatteryPercentCase" || key == "BatteryPercentLeft" || key == "BatteryPercentRight" else {
|
||||
return
|
||||
}
|
||||
|
||||
batteryLevel.append(KeyValue_t(key: key, value: "\(value)"))
|
||||
}
|
||||
}
|
||||
|
||||
return (uuid, batteryLevel)
|
||||
}
|
||||
|
||||
// MARK: - CBCentralManagerDelegate
|
||||
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
if central.state == .poweredOff {
|
||||
self.manager.stopScan()
|
||||
} else if central.state == .poweredOn {
|
||||
self.manager.scanForPeripherals(withServices: nil, options: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
guard let idx = self.devices.firstIndex(where: { $0.uuid == peripheral.identifier }) else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.devices[idx].RSSI == nil {
|
||||
self.devices[idx].RSSI = Int(truncating: RSSI)
|
||||
}
|
||||
|
||||
if self.devices[idx].peripheral == nil {
|
||||
self.devices[idx].peripheral = peripheral
|
||||
}
|
||||
|
||||
if peripheral.state == .disconnected {
|
||||
central.connect(peripheral, options: nil)
|
||||
} else if peripheral.state == .connected && !self.devices[idx].isInitialized {
|
||||
peripheral.delegate = self
|
||||
peripheral.discoverServices([batteryServiceUUID])
|
||||
self.devices[idx].isInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBPeripheralDelegate
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
guard error == nil else {
|
||||
print("didDiscoverServices: ", error!)
|
||||
return
|
||||
}
|
||||
|
||||
guard let service = peripheral.services?.first(where: { $0.uuid == self.batteryServiceUUID }) else {
|
||||
print("battery service not found, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
peripheral.discoverCharacteristics([self.batteryCharacteristicsUUID], for: service)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
guard error == nil else {
|
||||
print("didDiscoverCharacteristicsFor: ", error!)
|
||||
return
|
||||
}
|
||||
|
||||
guard let batteryCharacteristics = service.characteristics?.first(where: { $0.uuid == self.batteryCharacteristicsUUID }) else {
|
||||
print("characteristics not found")
|
||||
return
|
||||
}
|
||||
|
||||
self.characteristicsDict[peripheral.identifier] = batteryCharacteristics
|
||||
peripheral.readValue(for: batteryCharacteristics)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
guard error == nil else {
|
||||
print("didUpdateValueFor: ", error!)
|
||||
return
|
||||
}
|
||||
|
||||
if let batteryLevel = characteristic.value?[0], let idx = self.devices.firstIndex(where: { $0.uuid == peripheral.identifier }) {
|
||||
self.devices[idx].batteryLevel = [KeyValue_t(key: "battery", value: "\(batteryLevel)")]
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Modules/Bluetooth/settings.swift
Normal file
112
Modules/Bluetooth/settings.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// settings.swift
|
||||
// Bluetooth
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 07/07/2021.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Kit
|
||||
|
||||
internal class Settings: NSStackView, Settings_v {
|
||||
public var callback: (() -> Void) = {}
|
||||
public var selectedBatteryHandler: (String) -> Void = {_ in }
|
||||
|
||||
private let title: String
|
||||
private var selectedBattery: String
|
||||
private var button: NSPopUpButton?
|
||||
|
||||
public init(_ title: String) {
|
||||
self.title = title
|
||||
self.selectedBattery = Store.shared.string(key: "\(self.title)_battery", defaultValue: "")
|
||||
|
||||
super.init(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: Constants.Settings.width - (Constants.Settings.margin*2),
|
||||
height: 0
|
||||
))
|
||||
|
||||
self.orientation = .vertical
|
||||
self.distribution = .gravityAreas
|
||||
self.edgeInsets = NSEdgeInsets(
|
||||
top: Constants.Settings.margin,
|
||||
left: Constants.Settings.margin,
|
||||
bottom: Constants.Settings.margin,
|
||||
right: Constants.Settings.margin
|
||||
)
|
||||
self.spacing = Constants.Settings.margin
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
internal func load(widgets: [widget_t]) {
|
||||
self.subviews.forEach{ $0.removeFromSuperview() }
|
||||
|
||||
self.addArrangedSubview(self.deviceSelector())
|
||||
|
||||
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing + self.edgeInsets.top + self.edgeInsets.bottom
|
||||
if self.frame.size.height != h {
|
||||
self.setFrameSize(NSSize(width: self.bounds.width, height: h))
|
||||
}
|
||||
}
|
||||
|
||||
private func deviceSelector() -> NSView {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width - Constants.Settings.margin*2, height: Constants.Settings.row))
|
||||
|
||||
let rowTitle: NSTextField = LabelField(
|
||||
frame: NSRect(x: 0, y: (view.frame.height - 16)/2, width: view.frame.width - 52, height: 17),
|
||||
localizedString("Battery to show")
|
||||
)
|
||||
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
rowTitle.textColor = .textColor
|
||||
|
||||
self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 140, y: -1, width: 140, height: 30))
|
||||
self.button!.target = self
|
||||
self.button?.action = #selector(self.handleSelection)
|
||||
|
||||
view.addSubview(rowTitle)
|
||||
view.addSubview(self.button!)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
internal func setList(_ list: [BLEDevice]) {
|
||||
var batteries: [String] = []
|
||||
list.forEach { (d: BLEDevice) in
|
||||
if d.batteryLevel.count == 1 {
|
||||
batteries.append(d.name)
|
||||
} else {
|
||||
d.batteryLevel.forEach { (pair: KeyValue_t) in
|
||||
batteries.append("\(d.name)@\(pair.key)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
if self.button?.itemTitles.count != batteries.count {
|
||||
self.button?.removeAllItems()
|
||||
}
|
||||
|
||||
if batteries != self.button?.itemTitles {
|
||||
self.button?.addItems(withTitles: batteries.map{ $0.replacingOccurrences(of: "@", with: " - ")})
|
||||
if self.selectedBattery != "" {
|
||||
self.button?.selectItem(withTitle: self.selectedBattery.replacingOccurrences(of: "@", with: " - "))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func handleSelection(_ sender: NSPopUpButton) {
|
||||
guard let item = sender.selectedItem else { return }
|
||||
self.selectedBattery = item.title.replacingOccurrences(of: " - ", with: "@")
|
||||
Store.shared.set(key: "\(self.title)_battery", value: self.selectedBattery)
|
||||
self.selectedBatteryHandler(self.selectedBattery)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user