Permalink
Browse files

Changed look to match old Apple battery gauge exactly.

Images are loaded only once at startup and cached.
  • Loading branch information...
1 parent 13c1764 commit 0fe0ba17f254df501bab83502001f1d4fc02ce33 @c-alpha committed Aug 24, 2012
Showing with 123 additions and 43 deletions.
  1. +123 −43 Battery Time Remaining/AppDelegate.m
View
166 Battery Time Remaining/AppDelegate.m
@@ -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,14 +28,22 @@ 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;
- (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];

0 comments on commit 0fe0ba1

Please sign in to comment.