Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Changed look to match old Apple battery gauge exactly.

Images are loaded only once at startup and cached.
  • Loading branch information...
commit 0fe0ba17f254df501bab83502001f1d4fc02ce33 1 parent 13c1764
c-alpha authored
Showing with 123 additions and 43 deletions.
  1. +123 −43 Battery Time Remaining/AppDelegate.m
166 Battery Time Remaining/AppDelegate.m
View
@@ -15,6 +15,11 @@
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <QuartzCore/QuartzCore.h>
+// In Apple's battery gauge, the battery icon is rendered further down from the
+// top than NSStatusItem does it. Hence we add an extra top offset to get the
+// exact same look.
+#define EXTRA_TOP_OFFSET 2.0f
+
// IOPS notification callback on power source change
static void PowerSourceChanged(void * context)
{
@@ -23,6 +28,13 @@ static void PowerSourceChanged(void * context)
[self updateStatusItem];
}
+@interface AppDelegate () {
+ NSDictionary *m_images;
+}
+- (void)cacheNamedImages;
+- (NSImage *)loadBatteryIconNamed:(NSString *)iconName;
+@end
+
@implementation AppDelegate
@synthesize statusItem, notifications, previousPercent;
@@ -30,7 +42,8 @@ @implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.advancedSupported = ([self getAdvancedBatteryInfo] != nil);
-
+ [self cacheNamedImages];
+
// Init notification
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
[self loadNotificationSetting];
@@ -124,7 +137,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
statusItem.highlightMode = YES;
statusItem.menu = statusBarMenu;
[self updateStatusItem];
-
+
// Capture Power Source updates and make sure our callback is called
CFRunLoopSourceRef loop = IOPSNotificationCreateRunLoopSource(PowerSourceChanged, (__bridge void *)self);
CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, kCFRunLoopDefaultMode);
@@ -177,7 +190,7 @@ - (void)updateStatusItem
NSInteger minute = timeTilCharged % 60;
// Return the time remaining string
- [self setStatusBarImage:[self getBatteryIconNamed:@"BatteryCharging"] title:[NSString stringWithFormat:@" %ld:%02ld", hour, minute]];
+ [self setStatusBarImage:[self getBatteryIconNamed:@"BatteryCharging"] title:[NSString stringWithFormat:@" (%ld:%02ld)", hour, minute]];
}
else
{
@@ -187,7 +200,7 @@ - (void)updateStatusItem
else
{
// Not charging and on a endless powersource
- [self setStatusBarImage:[self getBatteryIconNamed:@"BatteryCharged"] title:@""];
+ [self setStatusBarImage:[self getBatteryIconNamed:@"BatteryCharged"] title:@"Not Charging"];
NSNumber *currentBatteryCapacity = CFDictionaryGetValue(description, CFSTR(kIOPSCurrentCapacityKey));
NSNumber *maxBatteryCapacity = CFDictionaryGetValue(description, CFSTR(kIOPSMaxCapacityKey));
@@ -217,7 +230,7 @@ - (void)updateStatusItem
NSInteger minute = (int)timeRemaining % 3600 / 60;
// Return the time remaining string
- [self setStatusBarImage:[self getBatteryIconPercent:self.currentPercent] title:[NSString stringWithFormat:@" %ld:%02ld", hour, minute]];
+ [self setStatusBarImage:[self getBatteryIconPercent:self.currentPercent] title:[NSString stringWithFormat:@" (%ld:%02ld)", hour, minute]];
for (NSString *key in self.notifications)
{
@@ -240,17 +253,16 @@ - (void)updateStatusItem
- (void)setStatusBarImage:(NSImage *)image title:(NSString *)title
{
// Image
- [image setTemplate:YES];
[self.statusItem setImage:image];
[self.statusItem setAlternateImage:[self imageInvertColor:image]];
// Title
- NSDictionary *attributedStyle = [NSDictionary dictionaryWithObjectsAndKeys:
- // Font
- [NSFont menuFontOfSize:12.5f],
- NSFontAttributeName,
- nil];
-
+ NSDictionary *attributedStyle = [NSDictionary dictionaryWithObjectsAndKeys:
+ // Font
+ [NSFont menuFontOfSize:12.0f],
+ NSFontAttributeName,
+ nil];
+
NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:attributedStyle];
self.statusItem.attributedTitle = attributedTitle;
}
@@ -282,44 +294,112 @@ - (NSDictionary *)getMoreAdvancedBatteryInfo
- (NSImage *)getBatteryIconPercent:(NSInteger)percent
{
- // Make dynamic battery icon
- NSImage *batteryDynamic = [self getBatteryIconNamed:@"BatteryEmpty"];
-
- [batteryDynamic lockFocus];
-
- NSRect sourceRect;
- sourceRect.origin = NSZeroPoint;
- sourceRect.origin.x += [batteryDynamic size].width / 100 * 15;
- sourceRect.origin.y += [batteryDynamic size].height / 50 * 15;
- sourceRect.size = [batteryDynamic size];
- sourceRect.size.width -= [batteryDynamic size].width / 100 * 43;
- sourceRect.size.height -= [batteryDynamic size].height / 50 * 30;
-
- sourceRect.size.width -= [batteryDynamic size].width / 100 * (60.f - (60.f / 100.f * percent));
-
- // Set different color at 15 percent
- if (percent > 15)
- {
- [[NSColor blackColor] set];
- }
- else
- {
- [[NSColor redColor] set];
- }
-
- NSRectFill(sourceRect);
-
- [batteryDynamic unlockFocus];
-
- return batteryDynamic;
+ //
+ // Mimic Apple's original battery icon using hires artwork
+ //
+ NSImage *batteryOutline = [self getBatteryIconNamed:@"BatteryEmpty"];
+ NSImage *batteryLevelLeft = nil;
+ NSImage *batteryLevelMiddle = nil;
+ NSImage *batteryLevelRight = nil;
+
+ if (percent >= 15) {
+ // draw black capacity bar
+ batteryLevelLeft = [self getBatteryIconNamed:@"BatteryLevelCapB-L"];
+ batteryLevelMiddle = [self getBatteryIconNamed:@"BatteryLevelCapB-M"];
+ batteryLevelRight = [self getBatteryIconNamed:@"BatteryLevelCapB-R"];
+ }
+ else {
+ // draw red capacity bar
+ batteryLevelLeft = [self getBatteryIconNamed:@"BatteryLevelCapR-L"];
+ batteryLevelMiddle = [self getBatteryIconNamed:@"BatteryLevelCapR-M"];
+ batteryLevelRight = [self getBatteryIconNamed:@"BatteryLevelCapR-R"];
+ }
+
+ const CGFloat capBarLeftOffset = 3.0f * [batteryLevelLeft size].width;
+ CGFloat capBarHeight = [batteryLevelLeft size].height;
+ CGFloat capBarTopOffset = (([batteryOutline size].height - EXTRA_TOP_OFFSET) - capBarHeight) / 2.0;
+ CGFloat capBarLength = floor(percent / 7.6f); // max width is 13px
+ capBarLength = (capBarLength < (2 * [batteryLevelLeft size].width)) ? (2 * [batteryLevelLeft size].width) : capBarLength;
+
+ [batteryOutline lockFocus];
+ [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
+ NSDrawThreePartImage(NSMakeRect(capBarLeftOffset, capBarTopOffset, capBarLength, capBarHeight),
+ batteryLevelLeft, batteryLevelMiddle, batteryLevelRight,
+ NO,
+ NSCompositeDestinationOver, //NSCompositeSourceOver,
+ 0.94f,
+ NO);
+ [batteryOutline unlockFocus];
+
+ return batteryOutline;
}
-- (NSImage *)getBatteryIconNamed:(NSString *)iconName
+- (NSImage *)getBatteryIconNamed:(NSString *)iconName {
+ return [m_images objectForKey:iconName];
+}
+
+- (NSImage *)loadBatteryIconNamed:(NSString *)iconName
{
NSString *fileName = [NSString stringWithFormat:@"/System/Library/CoreServices/Menu Extras/Battery.menu/Contents/Resources/%@.pdf", iconName];
return [[NSImage alloc] initWithContentsOfFile:fileName];
}
+- (void)cacheNamedImages {
+ // special treatment for the BatteryCharging, BatteryCharged, and BatteryEmpty images
+ // they need to be shifted down by 1px to be in the same position as Apple's
+ NSSize newSize;
+ NSImage *origImg = nil;
+
+ origImg = [self loadBatteryIconNamed:@"BatteryCharging"];
+ newSize.width = origImg.size.width;
+ newSize.height = origImg.size.height + EXTRA_TOP_OFFSET;
+ NSImage *imgCharging = [[NSImage alloc] initWithSize:newSize];
+ [imgCharging lockFocus];
+ [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
+ [origImg drawInRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
+ fromRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
+ operation:NSCompositeSourceOver
+ fraction:1.0];
+ [imgCharging unlockFocus];
+
+ origImg = [self loadBatteryIconNamed:@"BatteryCharged"];
+ newSize.width = origImg.size.width;
+ newSize.height = origImg.size.height + EXTRA_TOP_OFFSET;
+ NSImage *imgCharged = [[NSImage alloc] initWithSize:newSize];
+ [imgCharged lockFocus];
+ [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
+ [origImg drawInRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
+ fromRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
+ operation:NSCompositeSourceOver
+ fraction:1.0];
+ [imgCharged unlockFocus];
+
+ origImg = [self loadBatteryIconNamed:@"BatteryEmpty"];
+ newSize.width = origImg.size.width;
+ newSize.height = origImg.size.height + EXTRA_TOP_OFFSET;
+ NSImage *imgEmpty = [[NSImage alloc] initWithSize:newSize];
+ [imgEmpty lockFocus];
+ [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
+ [origImg drawInRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
+ fromRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
+ operation:NSCompositeSourceOver
+ fraction:1.0];
+ [imgEmpty unlockFocus];
+
+ // finally construct the dictionary from which we will retrieve the images at runtime
+ m_images = [NSDictionary dictionaryWithObjectsAndKeys:
+ imgCharging, @"BatteryCharging",
+ imgCharged, @"BatteryCharged",
+ imgEmpty, @"BatteryEmpty",
+ [self loadBatteryIconNamed:@"BatteryLevelCapB-L"], @"BatteryLevelCapB-L",
+ [self loadBatteryIconNamed:@"BatteryLevelCapB-M"], @"BatteryLevelCapB-M",
+ [self loadBatteryIconNamed:@"BatteryLevelCapB-R"], @"BatteryLevelCapB-R",
+ [self loadBatteryIconNamed:@"BatteryLevelCapR-L"], @"BatteryLevelCapR-L",
+ [self loadBatteryIconNamed:@"BatteryLevelCapR-M"], @"BatteryLevelCapR-M",
+ [self loadBatteryIconNamed:@"BatteryLevelCapR-R"], @"BatteryLevelCapR-R",
+ nil];
+}
+
- (NSImage *)imageInvertColor:(NSImage *)_image
{
NSImage *image = [_image copy];
Please sign in to comment.
Something went wrong with that request. Please try again.