From c5ea5131d0c87bdfed6a30adc74a778da242bef5 Mon Sep 17 00:00:00 2001 From: Konstantin Tuev Date: Wed, 18 Nov 2020 23:12:38 +0200 Subject: [PATCH] Add playing now artwork switch, fix battery icon on Big Sur + delay + add preference option to show time instead of percentage, fix Big Sur issues with Clock and Lang --- Pock.xcodeproj/project.pbxproj | 36 +++ Pock/AppDelegate.swift | 2 +- Pock/BatteryTools/BatteryError.swift | 16 ++ Pock/BatteryTools/BatteryImageCache.swift | 25 ++ Pock/BatteryTools/BatteryService.swift | 240 ++++++++++++++++++ Pock/BatteryTools/BatteryState.swift | 54 ++++ Pock/BatteryTools/NSImage.swift | 24 ++ Pock/BatteryTools/RegistryKey.swift | 34 +++ Pock/BatteryTools/StatusBarIcon.swift | 153 +++++++++++ Pock/Extensions.swift | 5 +- .../ControlCenterWidgetPreferencePane.swift | 2 +- .../DockWidgetPreferencePane.swift | 2 +- .../GeneralPreferencePane.swift | 2 +- .../Base.lproj/NowPlayingPreferencePane.xib | 35 ++- .../NowPlayingPreferencePane.swift | 6 +- .../Base.lproj/StatusWidgetPreferencePane.xib | 36 ++- .../StatusWidgetPreferencePane.swift | 16 +- Pock/Preferences/Preferences.swift | 14 +- Pock/Private/MRMediaRemote.h | 1 + .../BatteryFill.imageset/Contents.json | 16 ++ .../BatteryFill.imageset/Fill.pdf | Bin 0 -> 3767 bytes .../BatteryFillCapLeft.imageset/CapLeft.pdf | Bin 0 -> 3760 bytes .../BatteryFillCapLeft.imageset/Contents.json | 16 ++ .../BatteryFillCapRight.imageset/CapRight.pdf | Bin 0 -> 3770 bytes .../Contents.json | 16 ++ .../BatteryOutline.imageset/Contents.json | 16 ++ .../BatteryOutline.imageset/Outline.pdf | Bin 0 -> 6078 bytes .../Charged and Plugged.pdf | Bin 0 -> 6594 bytes .../ChargedAndPlugged.imageset/Contents.json | 15 ++ .../Charging.imageset/Charging.pdf | Bin 0 -> 5902 bytes .../Charging.imageset/Contents.json | 16 ++ Pock/Various/Assets.xcassets/Contents.json | 6 +- .../DeadCropped.imageset/Contents.json | 16 ++ .../DeadCropped.imageset/Dead Cropped.pdf | Bin 0 -> 5997 bytes .../LowBattery.imageset/Contents.json | 16 ++ .../LowBattery.imageset/Low Battery.pdf | Bin 0 -> 5733 bytes .../None.imageset/Contents.json | 15 ++ .../Assets.xcassets/None.imageset/None.pdf | Bin 0 -> 5700 bytes Pock/Widgets/Dock/DockWidget.swift | 3 + .../Widgets/NowPlaying/NowPlayingHelper.swift | 74 +++--- .../NowPlaying/NowPlayingItemView.swift | 2 +- Pock/Widgets/Status/Items/SClockItem.swift | 7 +- Pock/Widgets/Status/Items/SPowerItem.swift | 162 +++++++++--- Pock/Widgets/Status/StatusWidget.swift | 18 +- 44 files changed, 991 insertions(+), 126 deletions(-) create mode 100644 Pock/BatteryTools/BatteryError.swift create mode 100644 Pock/BatteryTools/BatteryImageCache.swift create mode 100644 Pock/BatteryTools/BatteryService.swift create mode 100644 Pock/BatteryTools/BatteryState.swift create mode 100644 Pock/BatteryTools/NSImage.swift create mode 100644 Pock/BatteryTools/RegistryKey.swift create mode 100644 Pock/BatteryTools/StatusBarIcon.swift create mode 100644 Pock/Various/Assets.xcassets/BatteryFill.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/BatteryFill.imageset/Fill.pdf create mode 100644 Pock/Various/Assets.xcassets/BatteryFillCapLeft.imageset/CapLeft.pdf create mode 100644 Pock/Various/Assets.xcassets/BatteryFillCapLeft.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/BatteryFillCapRight.imageset/CapRight.pdf create mode 100644 Pock/Various/Assets.xcassets/BatteryFillCapRight.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/BatteryOutline.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/BatteryOutline.imageset/Outline.pdf create mode 100644 Pock/Various/Assets.xcassets/ChargedAndPlugged.imageset/Charged and Plugged.pdf create mode 100644 Pock/Various/Assets.xcassets/ChargedAndPlugged.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/Charging.imageset/Charging.pdf create mode 100644 Pock/Various/Assets.xcassets/Charging.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/DeadCropped.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/DeadCropped.imageset/Dead Cropped.pdf create mode 100644 Pock/Various/Assets.xcassets/LowBattery.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/LowBattery.imageset/Low Battery.pdf create mode 100644 Pock/Various/Assets.xcassets/None.imageset/Contents.json create mode 100644 Pock/Various/Assets.xcassets/None.imageset/None.pdf diff --git a/Pock.xcodeproj/project.pbxproj b/Pock.xcodeproj/project.pbxproj index 6c7f4353..17fadcba 100644 --- a/Pock.xcodeproj/project.pbxproj +++ b/Pock.xcodeproj/project.pbxproj @@ -84,6 +84,13 @@ 96BE2701237171DC0031B31F /* CCScreensaverItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BE2700237171DC0031B31F /* CCScreensaverItem.swift */; }; 96E7A152239B99F700CB4C4C /* CCDoNotDisturbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E7A151239B99F700CB4C4C /* CCDoNotDisturbItem.swift */; }; 96FE269F22D0C3810071645C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 96FE26A122D0C3810071645C /* Localizable.strings */; }; + 99510002256574DC00E34E4D /* BatteryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99510001256574DC00E34E4D /* BatteryService.swift */; }; + 99510005256574FD00E34E4D /* RegistryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99510004256574FD00E34E4D /* RegistryKey.swift */; }; + 995100092565752800E34E4D /* BatteryState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 995100072565752800E34E4D /* BatteryState.swift */; }; + 9951000A2565752800E34E4D /* BatteryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 995100082565752800E34E4D /* BatteryError.swift */; }; + 9951000F2565912200E34E4D /* BatteryImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9951000C2565912200E34E4D /* BatteryImageCache.swift */; }; + 995100102565912200E34E4D /* NSImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9951000D2565912200E34E4D /* NSImage.swift */; }; + 995100112565912200E34E4D /* StatusBarIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9951000E2565912200E34E4D /* StatusBarIcon.swift */; }; 9955665222DC668500EFFE6D /* CCVolumeMuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9955665122DC668500EFFE6D /* CCVolumeMuteItem.swift */; }; 999276BE249FD8C300EFF44A /* SLangItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 999276BD249FD8C300EFF44A /* SLangItem.swift */; }; /* End PBXBuildFile section */ @@ -210,6 +217,13 @@ 96F29996231797C00055DA26 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/GeneralPreferencePane.strings; sourceTree = ""; }; 96F2999A2317980B0055DA26 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ControlCenterWidgetPreferencePane.strings"; sourceTree = ""; }; 96FE26A022D0C3810071645C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 99510001256574DC00E34E4D /* BatteryService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryService.swift; sourceTree = ""; }; + 99510004256574FD00E34E4D /* RegistryKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistryKey.swift; sourceTree = ""; }; + 995100072565752800E34E4D /* BatteryState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryState.swift; sourceTree = ""; }; + 995100082565752800E34E4D /* BatteryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryError.swift; sourceTree = ""; }; + 9951000C2565912200E34E4D /* BatteryImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryImageCache.swift; sourceTree = ""; }; + 9951000D2565912200E34E4D /* NSImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSImage.swift; sourceTree = ""; }; + 9951000E2565912200E34E4D /* StatusBarIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarIcon.swift; sourceTree = ""; }; 9955665122DC668500EFFE6D /* CCVolumeMuteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CCVolumeMuteItem.swift; sourceTree = ""; }; 999276BD249FD8C300EFF44A /* SLangItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SLangItem.swift; sourceTree = ""; }; AAA3636F2333699C0016267D /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/DockFolderController.strings"; sourceTree = ""; }; @@ -320,6 +334,7 @@ 342D83932278D5CF000D79DA /* Pock */ = { isa = PBXGroup; children = ( + 995100132565B99100E34E4D /* BatteryTools */, 342D83B32278D5CF000D79DA /* AppDelegate.swift */, 3407F9DD22D0F0A60072E742 /* Extensions.swift */, 342D83942278D5CF000D79DA /* Interfaces UI */, @@ -639,6 +654,20 @@ name = Frameworks; sourceTree = ""; }; + 995100132565B99100E34E4D /* BatteryTools */ = { + isa = PBXGroup; + children = ( + 99510001256574DC00E34E4D /* BatteryService.swift */, + 995100082565752800E34E4D /* BatteryError.swift */, + 995100072565752800E34E4D /* BatteryState.swift */, + 99510004256574FD00E34E4D /* RegistryKey.swift */, + 9951000C2565912200E34E4D /* BatteryImageCache.swift */, + 9951000D2565912200E34E4D /* NSImage.swift */, + 9951000E2565912200E34E4D /* StatusBarIcon.swift */, + ); + path = BatteryTools; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -826,6 +855,7 @@ 342D83FA2278D5CF000D79DA /* SSpotlightItem.swift in Sources */, 342D83F92278D5CF000D79DA /* StatusItem.swift in Sources */, 999276BE249FD8C300EFF44A /* SLangItem.swift in Sources */, + 995100092565752800E34E4D /* BatteryState.swift in Sources */, 3406433E227ED8BC00981372 /* DockFolderItemView.swift in Sources */, 347F336022D0D68400FA9805 /* CCLockItem.swift in Sources */, 342D83E62278D5CF000D79DA /* StatusWidgetPreferencePane.swift in Sources */, @@ -844,26 +874,32 @@ 342D83F12278D5CF000D79DA /* CCVolumeDownItem.swift in Sources */, 342D84032278D5CF000D79DA /* FileEvent.swift in Sources */, 9675D6D323701BC900C36234 /* CCBrightnessToggleItem.swift in Sources */, + 995100112565912200E34E4D /* StatusBarIcon.swift in Sources */, 3407F9EB22D1C7F40072E742 /* AppExposeItem.swift in Sources */, 34E644F4227CB6CE003D0450 /* KeySender.swift in Sources */, 342D840B2278D5CF000D79DA /* DockItemView.swift in Sources */, 342D840C2278D5CF000D79DA /* DockWidget.swift in Sources */, 34E644FB227D9A69003D0450 /* DockWidgetPreferencePane.swift in Sources */, 3407F9DC22D0E75E0072E742 /* ControlCenterWidgetPreferencePane.swift in Sources */, + 99510002256574DC00E34E4D /* BatteryService.swift in Sources */, 342D83EE2278D5CF000D79DA /* ControlCenterWidget.swift in Sources */, 342D83EC2278D5CF000D79DA /* OSDUIHelper.swift in Sources */, + 995100102565912200E34E4D /* NSImage.swift in Sources */, 3418D9FF22882EC600E11E37 /* PKSlideableController.swift in Sources */, 342D83FE2278D5CF000D79DA /* EscWidget.swift in Sources */, 345C955423A53D4D00E1AC53 /* NowPlayingPreferencePane.swift in Sources */, 34064343227EDC2200981372 /* DockFolderRepository.swift in Sources */, 342D83E02278D5CF000D79DA /* PKTouchBarController.swift in Sources */, + 9951000F2565912200E34E4D /* BatteryImageCache.swift in Sources */, 3406434A227F323A00981372 /* PKTouchBarNavController.swift in Sources */, + 9951000A2565752800E34E4D /* BatteryError.swift in Sources */, 34064337227E224700981372 /* DockFolderController.swift in Sources */, 342D83F22278D5CF000D79DA /* CCBrightnessUpItem.swift in Sources */, 3407F9DE22D0F0A60072E742 /* Extensions.swift in Sources */, 342D83F02278D5CF000D79DA /* CCBrightnessDownItem.swift in Sources */, 347F336722D0DB9200FA9805 /* SystemHelper.m in Sources */, 342D83F52278D5CF000D79DA /* NowPlayingItem.swift in Sources */, + 99510005256574FD00E34E4D /* RegistryKey.swift in Sources */, 342D83F62278D5CF000D79DA /* NowPlayingWidget.swift in Sources */, 342D840D2278D5CF000D79DA /* FileMonitor.swift in Sources */, 3407F9E722D1C5170072E742 /* AppExposeController.swift in Sources */, diff --git a/Pock/AppDelegate.swift b/Pock/AppDelegate.swift index bb4352b1..d67c8b86 100644 --- a/Pock/AppDelegate.swift +++ b/Pock/AppDelegate.swift @@ -47,7 +47,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { /// Initialize Crashlytics if isProd { UserDefaults.standard.register(defaults: ["NSApplicationCrashOnExceptions": true]) - Fabric.with([Crashlytics.self]) + //Fabric.with([Crashlytics.self]) } /// Check for accessibility (needed for badges to work) diff --git a/Pock/BatteryTools/BatteryError.swift b/Pock/BatteryTools/BatteryError.swift new file mode 100644 index 00000000..8333d39f --- /dev/null +++ b/Pock/BatteryTools/BatteryError.swift @@ -0,0 +1,16 @@ +// +// BatteryError.swift +// Apple Juice +// https://github.com/raphaelhanneken/apple-juice +// + +/// Exceptions for the Battery class. +/// +/// - connectionAlreadyOpen: Get's thrown in case the connection to the battery's IOService +/// is already open. Accepts an error description of type String. +/// - serviceNotFound: Get's thrown in case the supplied IOService wasn't found. +/// Accepts an error description of type String. +enum BatteryError: Error { + case connectionAlreadyOpen(String) + case serviceNotFound(String) +} diff --git a/Pock/BatteryTools/BatteryImageCache.swift b/Pock/BatteryTools/BatteryImageCache.swift new file mode 100644 index 00000000..326da7d2 --- /dev/null +++ b/Pock/BatteryTools/BatteryImageCache.swift @@ -0,0 +1,25 @@ +// +// BatteryImageCache.swift +// Apple Juice +// https://github.com/raphaelhanneken/apple-juice +// + +import Cocoa + +struct BatteryImageCache { + + /// The cached battery icon. + let image: NSImage? + /// The BatteryState associated with the cached battery icon. + let batteryStatus: BatteryState + + /// Cache a battery icon alongside it's corresponding BatteryState. + /// + /// - parameter status: The BatteryState to cache the battery icon for. + /// - parameter img: The battery icon to cache. + init(forStatus status: BatteryState, withImage img: NSImage?) { + batteryStatus = status + image = img + } + +} diff --git a/Pock/BatteryTools/BatteryService.swift b/Pock/BatteryTools/BatteryService.swift new file mode 100644 index 00000000..fca5c78f --- /dev/null +++ b/Pock/BatteryTools/BatteryService.swift @@ -0,0 +1,240 @@ +// +// Battery.swift +// Apple Juice +// https://github.com/raphaelhanneken/apple-juice +// + +import Foundation +import IOKit.ps + +/// Notification name for the power source changed callback. +let powerSourceChangedNotification = "com.raphaelhanneken.apple-juice.powersourcechanged" + +/// Posts a notification every time the power source changes. +private let powerSourceCallback: IOPowerSourceCallbackType = { _ in + NotificationCenter.default.post(name: Notification.Name(rawValue: powerSourceChangedNotification), + object: nil) +} + +/// Accesses the battery's IO service. +final class BatteryService { + + /// Closed state value for the service connection object. + private static let connectionClosed: UInt32 = 0 + + /// An IOService object that matches battery's IO service dictionary. + private var service: io_object_t = BatteryService.connectionClosed + + /// The current status of the battery, e.g. charging. + var state: BatteryState? { + guard + let plugged = isPlugged, + let charged = isCharged, + let percentage = percentage else { + return nil + } + if charged && plugged { + return .chargedAndPlugged + } + if plugged { + return .charging(percentage: percentage) + } + + return .discharging(percentage: percentage) + } + + /// The remaining time until the battery is empty or fully charged + /// in a human readable format, e.g. hh:mm. + var timeRemainingFormatted: String { + // Unwrap required information. + guard let charged = isCharged, let plugged = isPlugged else { + return NSLocalizedString("-1:-1", comment: "") + } + // Check if the battery is charged and plugged into an unlimited power supply. + if charged && plugged { + return NSLocalizedString("Full", comment: "") + } + // The battery is (dis)charging, display the remaining time. + if let time = timeRemaining { + return String(format: "%d:%02d", arguments: [time / 60, time % 60]) + } + + return NSLocalizedString("Wait", comment: "") + } + + /// The remaining time in _minutes_ until the battery is empty or fully charged. + var timeRemaining: Int? { + // Get the estimated time remaining. + let time = IOPSGetTimeRemainingEstimate() + + switch time { + case kIOPSTimeRemainingUnknown: + return nil + case kIOPSTimeRemainingUnlimited: + // The battery is connected to a power outlet, get the remaining time + // until the battery is fully charged. + if let prop = getRegistryProperty(forKey: .timeRemaining) as? Int, prop < 600 { + return prop + } + return nil + default: + // The estimated time in minutes + return Int(time / 60) + } + } + + /// The current percentage, based on the current charge and the maximum capacity. + var percentage: Int? { + return getPowerSourceProperty(forKey: .percentage) as? Int + } + + /// The current percentage, formatted according to the selected client locale, e.g. + /// en_US: 42% fr_FR: 42 % + var percentageFormatted: String { + guard let percentage = self.percentage else { + return NSLocalizedString("Calculating", comment: "") + } + + let percentageFormatter = NumberFormatter() + percentageFormatter.numberStyle = .percent + percentageFormatter.generatesDecimalNumbers = false + percentageFormatter.localizesFormat = true + percentageFormatter.multiplier = 1.0 + percentageFormatter.minimumFractionDigits = 0 + percentageFormatter.maximumFractionDigits = 0 + + return percentageFormatter.string(from: percentage as NSNumber) ?? "\(percentage) %" + } + + /// The current charge in mAh. + var charge: Int? { + return getRegistryProperty(forKey: .currentCharge) as? Int + } + + /// The maximum capacity in mAh. + var capacity: Int? { + return getRegistryProperty(forKey: .maxCapacity) as? Int + } + + /// The source from which the Mac currently draws its power. + var powerSource: String { + guard let plugged = isPlugged else { + return NSLocalizedString("Unknown", comment: "") + } + // Check whether the MacBook currently is plugged into a power adapter. + if plugged { + return NSLocalizedString("Power Adapter", comment: "") + } + + return NSLocalizedString("Battery", comment: "") + } + + /// Checks whether the battery is charging and connected to a power outlet. + var isCharging: Bool? { + return getRegistryProperty(forKey: .isCharging) as? Bool + } + + /// Checks whether the battery is fully charged. + var isCharged: Bool? { + return getRegistryProperty(forKey: .fullyCharged) as? Bool + } + + /// Checks whether the battery is plugged into an unlimited power supply. + var isPlugged: Bool? { + return getRegistryProperty(forKey: .isPlugged) as? Bool + } + + /// Calculates the current power usage in Watts. + var powerUsage: Double? { + guard + let voltage = getRegistryProperty(forKey: .voltage) as? Double, + let amperage = getRegistryProperty(forKey: .amperage) as? Double else { + return nil + } + return round((voltage * amperage) / 1_000_000) + } + + /// Current flowing into or out of the battery. + var amperage: Int? { + guard + let amperage = getRegistryProperty(forKey: .amperage) as? Int else { + return nil + } + return amperage + } + + /// The number of charging cycles. + var cycleCount: Int? { + return getRegistryProperty(forKey: .cycleCount) as? Int + } + + /// The battery's current temperature. + var temperature: Double? { + guard let temp = getRegistryProperty(forKey: .temperature) as? Double else { + return nil + } + return (temp / 100) + } + + /// The batteries' health status + var health: String? { + return getPowerSourceProperty(forKey: .health) as? String + } + + /// Initializes a new Battery object. + init() throws { + try openServiceConnection() + CFRunLoopAddSource(CFRunLoopGetCurrent(), + IOPSNotificationCreateRunLoopSource(powerSourceCallback, nil).takeRetainedValue(), + CFRunLoopMode.defaultMode) + } + + /// Opens a connection to the battery's IOService object. + /// + /// - throws: A BatteryError if something went wrong. + private func openServiceConnection() throws { + if service != BatteryService.connectionClosed && !closeServiceConnection() { + // For some reason we have an open IO Service connection which we cannot close. + throw BatteryError.connectionAlreadyOpen("Closing the IOService connection failed.") + } + service = IOServiceGetMatchingService(kIOMasterPortDefault, + IOServiceNameMatching(RegistryKey.service.rawValue)) + + if service == BatteryService.connectionClosed { + throw BatteryError + .serviceNotFound("Opening the provided IOService (\(RegistryKey.service.rawValue)) failed.") + } + } + + /// Closes the connection the the battery's IOService object. + /// + /// - returns: True, when the IOService connection was successfully closed. + public func closeServiceConnection() -> Bool { + if kIOReturnSuccess == IOObjectRelease(service) { + service = BatteryService.connectionClosed + } + + return (service == BatteryService.connectionClosed) + } + + /// Get the registry entry's property for the supplied SmartBatteryKey. + /// + /// - parameter key: A SmartBatteryKey to get the corresponding registry entry's property. + /// - returns: The registry entry for the provided SmartBatteryKey. + private func getRegistryProperty(forKey key: RegistryKey) -> AnyObject? { + return IORegistryEntryCreateCFProperty(service, key.rawValue as CFString?, nil, 0) + .takeRetainedValue() + } + + private func getPowerSourceProperty(forKey key: RegistryKey) -> Any? { + let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue() + let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as? [CFDictionary] + + guard let powerSources = psList else { + return nil + } + let powerSource = powerSources[0] as NSDictionary + + return powerSource[key.rawValue] + } +} diff --git a/Pock/BatteryTools/BatteryState.swift b/Pock/BatteryTools/BatteryState.swift new file mode 100644 index 00000000..d04b8a93 --- /dev/null +++ b/Pock/BatteryTools/BatteryState.swift @@ -0,0 +1,54 @@ +// +// BatteryState.swift +// Apple Juice +// https://github.com/raphaelhanneken/apple-juice +// + +import Foundation + +/// Define the precision, with wich the icon can display the current charging level +public let drawingPrecision = 5.4 + +/// Defines the state the battery is currently in. +/// +/// - chargedAndPlugged: The battery is plugged into a power supply and charged. +/// - charging: The battery is plugged into a power supply and +/// charging. Takes the current percentage as argument. +/// - discharging: The battery is currently discharging. Accepts the +/// current percentage as argument. +enum BatteryState: Equatable { + case chargedAndPlugged + case charging(percentage: Int) + case discharging(percentage: Int) + + /// The current percentage. + var percentage: Int { + switch self { + case .chargedAndPlugged: + return 100 + case .charging(let percentage): + return percentage + case .discharging(let percentage): + return percentage + } + } +} + +/// Compares two BatteryStatusTypes for equality. +/// +/// - parameter lhs: A BatteryStatusType. +/// - parameter rhs: Another BatteryStatusType. +/// - returns: True if the supplied BatteryStatusType's are equal. Otherwise false. +func == (lhs: BatteryState, rhs: BatteryState) -> Bool { + switch (lhs, rhs) { + case (.charging, .charging), (.chargedAndPlugged, .chargedAndPlugged): + return true + case let (.discharging(lhsPercentage), .discharging(rhsPercentage)): + // Divide the percentages by the defined drawing precision; So that the battery image + // only gets redrawn, when it actually differs. + return round(Double(lhsPercentage) / drawingPrecision) + == round(Double(rhsPercentage) / drawingPrecision) + default: + return false + } +} diff --git a/Pock/BatteryTools/NSImage.swift b/Pock/BatteryTools/NSImage.swift new file mode 100644 index 00000000..a46ba1c1 --- /dev/null +++ b/Pock/BatteryTools/NSImage.swift @@ -0,0 +1,24 @@ +// +// PreferenceKeys.swift +// Apple Juice +// https://github.com/raphaelhanneken/apple-juice +// + +import Cocoa + +extension NSImage { + + func drawThreePartImage( + withStartCap startCap: NSImage, + fill: NSImage, + endCap: NSImage, + inFrame frame: NSRect + ) -> NSImage { + lockFocus() + NSDrawThreePartImage(frame, startCap, fill, endCap, false, .copy, 1, false) + unlockFocus() + + return self + } + +} diff --git a/Pock/BatteryTools/RegistryKey.swift b/Pock/BatteryTools/RegistryKey.swift new file mode 100644 index 00000000..2ce13d3a --- /dev/null +++ b/Pock/BatteryTools/RegistryKey.swift @@ -0,0 +1,34 @@ +// +// SmartBatteryKeys.swift +// Apple Juice +// https://github.com/raphaelhanneken/apple-juice +// + +/// Keys to lookup information from the IO Service dictionary 'ioreg -brc AppleSmartBattery'. +/// +/// - isPlugged: Is the battery connected to an external power source +/// - isCharging: The battery's charging state +/// - currentCharge: An estimate about the current charge in mAh +/// - maxCapacity: The maximun charging capacity in mAh +/// - fullyCharged: Is the battery's max charging capacity reached +/// - cycleCount: The number of charging cycles +/// - temperature: The temperature in degrees celsius +/// - voltage: The current voltage +/// - amperage: The current amperage +/// - timeRemaining: An estimate about the remaining time until the battery is fully charged or depleted +/// - service: The service name +enum RegistryKey: String { + case isPlugged = "ExternalConnected" + case isCharging = "IsCharging" + case currentCharge = "CurrentCapacity" + case maxCapacity = "MaxCapacity" + case fullyCharged = "FullyCharged" + case cycleCount = "CycleCount" + case temperature = "Temperature" + case voltage = "Voltage" + case amperage = "Amperage" + case timeRemaining = "TimeRemaining" + case service = "AppleSmartBattery" + case health = "BatteryHealth" + case percentage = "Current Capacity" +} diff --git a/Pock/BatteryTools/StatusBarIcon.swift b/Pock/BatteryTools/StatusBarIcon.swift new file mode 100644 index 00000000..c9b1c3d3 --- /dev/null +++ b/Pock/BatteryTools/StatusBarIcon.swift @@ -0,0 +1,153 @@ +// +// StatusBarIcon.swift +// Apple Juice +// https://github.com/raphaelhanneken/apple-juice +// + +import Cocoa + +/// Image names for the images used by the menu bar item icon. +private enum BatteryImage: NSImage.Name { + case left = "BatteryFillCapLeft" + case right = "BatteryFillCapRight" + case middle = "BatteryFill" + case outline = "BatteryOutline" + case charging = "Charging" + case chargingSymbol = "ChargingSymbol" + case chargedAndPlugged = "ChargedAndPlugged" + case deadCropped = "DeadCropped" + case none = "None" + case lowBattery = "LowBattery" +} + +internal struct StatusBarIcon { + + /// The little margins between the battery outline and the capcity bar. + private let capacityOffsetX: CGFloat = 2.0 + private let capacityOffsetY: CGFloat = 2.0 + + /// Cache the last drawn battery icon. + private var cache: BatteryImageCache? + + /// Draws a battery icon for the given BatteryState. + /// + /// - parameter status: The BatteryState for the status the battery is currently in, e.g. charging + /// - returns: The battery image for the provided battery status. + mutating internal func drawBatteryImage(forStatus status: BatteryState) -> NSImage? { + if let cache = self.cache, cache.batteryStatus == status { + return cache.image + } + + switch status { + case .charging: + cache = BatteryImageCache(forStatus: status, + withImage: batteryImage(named: .charging)) + case .chargedAndPlugged: + cache = BatteryImageCache(forStatus: status, + withImage: batteryImage(named: .chargedAndPlugged)) + case let .discharging(percentage): + cache = BatteryImageCache(forStatus: status, + withImage: dischargingBatteryImage(forPercentage: Double(percentage))) + } + + return cache?.image + } + + /// Draws a battery icon for the given BatteryError. + /// + /// - parameter err: The BatteryError object for the corresponding error that happened. + /// - returns: A battery icon for the given BatteryError. + internal func drawBatteryImage(forError error: BatteryError?) -> NSImage? { + guard let error = error else { return nil } + + switch error { + case .connectionAlreadyOpen: + return batteryImage(named: .deadCropped) + case .serviceNotFound: + return batteryImage(named: .none) + } + } + + /// Draws a battery icon based on the battery's current percentage. + /// + /// - parameter percentage: The current percentage of the battery. + /// - returns: A battery icon for the supplied percentage. + private func dischargingBatteryImage(forPercentage percentage: Double) -> NSImage? { + guard let batteryOutline = batteryImage(named: .outline), + let capacityCapLeft = batteryImage(named: .left), + let capacityCapRight = batteryImage(named: .right), + let capacityFill = batteryImage(named: .middle) else { + return nil + } + + // Delete the image name for the battery outline to keep it's representations out of the NSCachedImageRep + batteryOutline.setName(nil) + let drawingRect = NSRect(x: capacityOffsetX, + y: capacityOffsetY, + width: CGFloat(round(percentage / drawingPrecision)) * capacityFill.size.width, + height: capacityFill.size.height) + + // NSImage#drawThreePartImage glitchets when the width of the capacity bar drops + // below the combined width of startCap and endCap. + if drawingRect.width < (2 * capacityFill.size.width) { + return batteryImage(named: .lowBattery) + } + + return batteryOutline.drawThreePartImage(withStartCap: capacityCapLeft, + fill: capacityFill, + endCap: capacityCapRight, + inFrame: drawingRect) + } + + /// Draws a battery icon based on the battery's current percentage. + /// + /// - parameter percentage: The current percentage of the battery. + /// - returns: A battery icon for the supplied percentage. + private func chargingBatteryImage(forPercentage percentage: Double) -> NSImage? { + guard let batteryOutline = batteryImage(named: .outline), + let capacityCapLeft = batteryImage(named: .left), + let capacityCapRight = batteryImage(named: .right), + let capacityFill = batteryImage(named: .middle) else { + return nil + } + + // Delete the image name for the battery outline to keep it's representations out of the NSCachedImageRep + batteryOutline.setName(nil) + batteryOutline.isTemplate = false + let drawingRect = NSRect(x: capacityOffsetX, + y: capacityOffsetY, + width: CGFloat(round(percentage / drawingPrecision)) * capacityFill.size.width, + height: capacityFill.size.height) + + // NSImage#drawThreePartImage glitchets when the width of the capacity bar drops + // below the combined width of startCap and endCap. + if drawingRect.width < (2 * capacityFill.size.width) { + return batteryImage(named: .lowBattery) + } + + batteryOutline.drawThreePartImage(withStartCap: capacityCapLeft, + fill: capacityFill, + endCap: capacityCapRight, + inFrame: drawingRect).tint(color: NSColor.white, noCopy: true) + + if let image = batteryImage(named: .chargingSymbol)?.tint(color: NSColor.black) { + batteryOutline.lockFocus() + image.draw(in: CGRect(x: (batteryOutline.size.width-image.size.width)/2, y: (batteryOutline.size.height-image.size.height)/2, width: image.size.width, height: image.size.height)) + batteryOutline.unlockFocus() + } + + return batteryOutline + } + + /// Returns the image object associated with the specified name as template. + /// + /// - parameter name: The name of an image in the app bundle. + /// - returns: An image object associated with the specified name as template. + private func batteryImage(named name: BatteryImage) -> NSImage? { + guard let img = NSImage(named: name.rawValue) else { return nil } + img.isTemplate = true + + return img + } + +} diff --git a/Pock/Extensions.swift b/Pock/Extensions.swift index a34d578c..cff16732 100644 --- a/Pock/Extensions.swift +++ b/Pock/Extensions.swift @@ -18,13 +18,14 @@ extension NSImage { newImage.size = destSize return NSImage(data: newImage.tiffRepresentation!)!.tint(color: color) } - func tint(color: NSColor) -> NSImage { - let image = self.copy() as! NSImage + func tint(color: NSColor, noCopy: Bool = false) -> NSImage { + let image = noCopy ? self : self.copy() as! NSImage image.lockFocus() color.set() let imageRect = NSRect(origin: NSZeroPoint, size: image.size) imageRect.fill(using: .sourceAtop) image.unlockFocus() + image.isTemplate = false return image } } diff --git a/Pock/Preferences/Panes/ControlCenterWidgetPreferencePane/ControlCenterWidgetPreferencePane.swift b/Pock/Preferences/Panes/ControlCenterWidgetPreferencePane/ControlCenterWidgetPreferencePane.swift index 1fb3fd8b..2e4c1b89 100644 --- a/Pock/Preferences/Panes/ControlCenterWidgetPreferencePane/ControlCenterWidgetPreferencePane.swift +++ b/Pock/Preferences/Panes/ControlCenterWidgetPreferencePane/ControlCenterWidgetPreferencePane.swift @@ -27,7 +27,7 @@ class ControlCenterWidgetPreferencePane: NSViewController, PreferencePane { @IBOutlet weak var showVolumeToggleItem: NSButton! /// Preferenceable - var preferencePaneIdentifier: Identifier = Identifier.controler_center_widget + var preferencePaneIdentifier: Preferences.PaneIdentifier = Preferences.PaneIdentifier.controler_center_widget let preferencePaneTitle: String = "Control Center Widget".localized var toolbarItemIcon: NSImage = NSImage(named: "ControlCenterWidget")! diff --git a/Pock/Preferences/Panes/DockWidgetPreferencePane/DockWidgetPreferencePane.swift b/Pock/Preferences/Panes/DockWidgetPreferencePane/DockWidgetPreferencePane.swift index 87b5ec12..b617cf00 100644 --- a/Pock/Preferences/Panes/DockWidgetPreferencePane/DockWidgetPreferencePane.swift +++ b/Pock/Preferences/Panes/DockWidgetPreferencePane/DockWidgetPreferencePane.swift @@ -22,7 +22,7 @@ class DockWidgetPreferencePane: NSViewController, PreferencePane { @IBOutlet weak var itemSpacingTextField: NSTextField! /// Preferenceable - var preferencePaneIdentifier: Identifier = Identifier.dock_widget + var preferencePaneIdentifier: Preferences.PaneIdentifier = Preferences.PaneIdentifier.dock_widget let preferencePaneTitle: String = "Dock Widget".localized var toolbarItemIcon: NSImage { diff --git a/Pock/Preferences/Panes/GeneralPreferencePane/GeneralPreferencePane.swift b/Pock/Preferences/Panes/GeneralPreferencePane/GeneralPreferencePane.swift index d5559b9c..bd7eb501 100644 --- a/Pock/Preferences/Panes/GeneralPreferencePane/GeneralPreferencePane.swift +++ b/Pock/Preferences/Panes/GeneralPreferencePane/GeneralPreferencePane.swift @@ -34,7 +34,7 @@ final class GeneralPreferencePane: NSViewController, PreferencePane { private static let appVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String ?? "Unknown" /// Preferenceable - var preferencePaneIdentifier: Identifier = Identifier.general + var preferencePaneIdentifier: Preferences.PaneIdentifier = Preferences.PaneIdentifier.general let preferencePaneTitle: String = "General".localized let toolbarItemIcon: NSImage = NSImage(named: NSImage.preferencesGeneralName)! diff --git a/Pock/Preferences/Panes/NowPlayingPreferencePane/Base.lproj/NowPlayingPreferencePane.xib b/Pock/Preferences/Panes/NowPlayingPreferencePane/Base.lproj/NowPlayingPreferencePane.xib index 781b4c97..02e6bf3c 100644 --- a/Pock/Preferences/Panes/NowPlayingPreferencePane/Base.lproj/NowPlayingPreferencePane.xib +++ b/Pock/Preferences/Panes/NowPlayingPreferencePane/Base.lproj/NowPlayingPreferencePane.xib @@ -1,8 +1,8 @@ - + - + @@ -14,17 +14,18 @@ + - + - + @@ -72,7 +73,7 @@ - + @@ -80,7 +81,7 @@ - + + + + diff --git a/Pock/Preferences/Panes/NowPlayingPreferencePane/NowPlayingPreferencePane.swift b/Pock/Preferences/Panes/NowPlayingPreferencePane/NowPlayingPreferencePane.swift index 7320d97a..9092ea98 100644 --- a/Pock/Preferences/Panes/NowPlayingPreferencePane/NowPlayingPreferencePane.swift +++ b/Pock/Preferences/Panes/NowPlayingPreferencePane/NowPlayingPreferencePane.swift @@ -13,7 +13,7 @@ import Defaults class NowPlayingPreferencePane: NSViewController, PreferencePane { /// Preferenceable - var preferencePaneIdentifier: Identifier = Identifier.now_playing_widget + var preferencePaneIdentifier: Preferences.PaneIdentifier = Preferences.PaneIdentifier.now_playing_widget let preferencePaneTitle: String = "Now Playing Widget".localized var toolbarItemIcon: NSImage { let id: String @@ -33,6 +33,7 @@ class NowPlayingPreferencePane: NSViewController, PreferencePane { @IBOutlet private weak var playPauseRadioButton: NSButton! @IBOutlet private weak var hideWidgetIfNoMedia: NSButton! @IBOutlet private weak var animateIconWhilePlaying: NSButton! + @IBOutlet private weak var showArtwork: NSButton! override var nibName: NSNib.Name? { return "NowPlayingPreferencePane" @@ -50,6 +51,7 @@ class NowPlayingPreferencePane: NSViewController, PreferencePane { } hideWidgetIfNoMedia.state = Defaults[.hideNowPlayingIfNoMedia] ? .on : .off animateIconWhilePlaying.state = Defaults[.animateIconWhilePlaying] ? .on : .off + showArtwork.state = Defaults[.showArtwork] ? .on : .off setupImageViewClickGesture() } @@ -92,6 +94,8 @@ class NowPlayingPreferencePane: NSViewController, PreferencePane { Defaults[.hideNowPlayingIfNoMedia] = button.state == .on case 1: Defaults[.animateIconWhilePlaying] = button.state == .on + case 2: + Defaults[.showArtwork] = button.state == .on default: return } diff --git a/Pock/Preferences/Panes/StatusWidgetPreferencePane/Base.lproj/StatusWidgetPreferencePane.xib b/Pock/Preferences/Panes/StatusWidgetPreferencePane/Base.lproj/StatusWidgetPreferencePane.xib index dc629d5c..d2d10439 100644 --- a/Pock/Preferences/Panes/StatusWidgetPreferencePane/Base.lproj/StatusWidgetPreferencePane.xib +++ b/Pock/Preferences/Panes/StatusWidgetPreferencePane/Base.lproj/StatusWidgetPreferencePane.xib @@ -1,8 +1,8 @@ - + - + @@ -10,6 +10,7 @@ + @@ -20,14 +21,14 @@ - + - + + - + @@ -99,7 +110,7 @@