Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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 1 changed file with 123 additions and 43 deletions. Show diff stats Hide diff stats

  1. +123 43 Battery Time Remaining/AppDelegate.m
166 Battery Time Remaining/AppDelegate.m
@@ -15,6 +15,11 @@
15 15 #import <IOKit/pwr_mgt/IOPMLib.h>
16 16 #import <QuartzCore/QuartzCore.h>
17 17
  18 +// In Apple's battery gauge, the battery icon is rendered further down from the
  19 +// top than NSStatusItem does it. Hence we add an extra top offset to get the
  20 +// exact same look.
  21 +#define EXTRA_TOP_OFFSET 2.0f
  22 +
18 23 // IOPS notification callback on power source change
19 24 static void PowerSourceChanged(void * context)
20 25 {
@@ -23,6 +28,13 @@ static void PowerSourceChanged(void * context)
23 28 [self updateStatusItem];
24 29 }
25 30
  31 +@interface AppDelegate () {
  32 + NSDictionary *m_images;
  33 +}
  34 +- (void)cacheNamedImages;
  35 +- (NSImage *)loadBatteryIconNamed:(NSString *)iconName;
  36 +@end
  37 +
26 38 @implementation AppDelegate
27 39
28 40 @synthesize statusItem, notifications, previousPercent;
@@ -30,7 +42,8 @@ @implementation AppDelegate
30 42 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
31 43 {
32 44 self.advancedSupported = ([self getAdvancedBatteryInfo] != nil);
33   -
  45 + [self cacheNamedImages];
  46 +
34 47 // Init notification
35 48 [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
36 49 [self loadNotificationSetting];
@@ -124,7 +137,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
124 137 statusItem.highlightMode = YES;
125 138 statusItem.menu = statusBarMenu;
126 139 [self updateStatusItem];
127   -
  140 +
128 141 // Capture Power Source updates and make sure our callback is called
129 142 CFRunLoopSourceRef loop = IOPSNotificationCreateRunLoopSource(PowerSourceChanged, (__bridge void *)self);
130 143 CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, kCFRunLoopDefaultMode);
@@ -177,7 +190,7 @@ - (void)updateStatusItem
177 190 NSInteger minute = timeTilCharged % 60;
178 191
179 192 // Return the time remaining string
180   - [self setStatusBarImage:[self getBatteryIconNamed:@"BatteryCharging"] title:[NSString stringWithFormat:@" %ld:%02ld", hour, minute]];
  193 + [self setStatusBarImage:[self getBatteryIconNamed:@"BatteryCharging"] title:[NSString stringWithFormat:@" (%ld:%02ld)", hour, minute]];
181 194 }
182 195 else
183 196 {
@@ -187,7 +200,7 @@ - (void)updateStatusItem
187 200 else
188 201 {
189 202 // Not charging and on a endless powersource
190   - [self setStatusBarImage:[self getBatteryIconNamed:@"BatteryCharged"] title:@""];
  203 + [self setStatusBarImage:[self getBatteryIconNamed:@"BatteryCharged"] title:@"Not Charging"];
191 204
192 205 NSNumber *currentBatteryCapacity = CFDictionaryGetValue(description, CFSTR(kIOPSCurrentCapacityKey));
193 206 NSNumber *maxBatteryCapacity = CFDictionaryGetValue(description, CFSTR(kIOPSMaxCapacityKey));
@@ -217,7 +230,7 @@ - (void)updateStatusItem
217 230 NSInteger minute = (int)timeRemaining % 3600 / 60;
218 231
219 232 // Return the time remaining string
220   - [self setStatusBarImage:[self getBatteryIconPercent:self.currentPercent] title:[NSString stringWithFormat:@" %ld:%02ld", hour, minute]];
  233 + [self setStatusBarImage:[self getBatteryIconPercent:self.currentPercent] title:[NSString stringWithFormat:@" (%ld:%02ld)", hour, minute]];
221 234
222 235 for (NSString *key in self.notifications)
223 236 {
@@ -240,17 +253,16 @@ - (void)updateStatusItem
240 253 - (void)setStatusBarImage:(NSImage *)image title:(NSString *)title
241 254 {
242 255 // Image
243   - [image setTemplate:YES];
244 256 [self.statusItem setImage:image];
245 257 [self.statusItem setAlternateImage:[self imageInvertColor:image]];
246 258
247 259 // Title
248   - NSDictionary *attributedStyle = [NSDictionary dictionaryWithObjectsAndKeys:
249   - // Font
250   - [NSFont menuFontOfSize:12.5f],
251   - NSFontAttributeName,
252   - nil];
253   -
  260 + NSDictionary *attributedStyle = [NSDictionary dictionaryWithObjectsAndKeys:
  261 + // Font
  262 + [NSFont menuFontOfSize:12.0f],
  263 + NSFontAttributeName,
  264 + nil];
  265 +
254 266 NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:attributedStyle];
255 267 self.statusItem.attributedTitle = attributedTitle;
256 268 }
@@ -282,44 +294,112 @@ - (NSDictionary *)getMoreAdvancedBatteryInfo
282 294
283 295 - (NSImage *)getBatteryIconPercent:(NSInteger)percent
284 296 {
285   - // Make dynamic battery icon
286   - NSImage *batteryDynamic = [self getBatteryIconNamed:@"BatteryEmpty"];
287   -
288   - [batteryDynamic lockFocus];
289   -
290   - NSRect sourceRect;
291   - sourceRect.origin = NSZeroPoint;
292   - sourceRect.origin.x += [batteryDynamic size].width / 100 * 15;
293   - sourceRect.origin.y += [batteryDynamic size].height / 50 * 15;
294   - sourceRect.size = [batteryDynamic size];
295   - sourceRect.size.width -= [batteryDynamic size].width / 100 * 43;
296   - sourceRect.size.height -= [batteryDynamic size].height / 50 * 30;
297   -
298   - sourceRect.size.width -= [batteryDynamic size].width / 100 * (60.f - (60.f / 100.f * percent));
299   -
300   - // Set different color at 15 percent
301   - if (percent > 15)
302   - {
303   - [[NSColor blackColor] set];
304   - }
305   - else
306   - {
307   - [[NSColor redColor] set];
308   - }
309   -
310   - NSRectFill(sourceRect);
311   -
312   - [batteryDynamic unlockFocus];
313   -
314   - return batteryDynamic;
  297 + //
  298 + // Mimic Apple's original battery icon using hires artwork
  299 + //
  300 + NSImage *batteryOutline = [self getBatteryIconNamed:@"BatteryEmpty"];
  301 + NSImage *batteryLevelLeft = nil;
  302 + NSImage *batteryLevelMiddle = nil;
  303 + NSImage *batteryLevelRight = nil;
  304 +
  305 + if (percent >= 15) {
  306 + // draw black capacity bar
  307 + batteryLevelLeft = [self getBatteryIconNamed:@"BatteryLevelCapB-L"];
  308 + batteryLevelMiddle = [self getBatteryIconNamed:@"BatteryLevelCapB-M"];
  309 + batteryLevelRight = [self getBatteryIconNamed:@"BatteryLevelCapB-R"];
  310 + }
  311 + else {
  312 + // draw red capacity bar
  313 + batteryLevelLeft = [self getBatteryIconNamed:@"BatteryLevelCapR-L"];
  314 + batteryLevelMiddle = [self getBatteryIconNamed:@"BatteryLevelCapR-M"];
  315 + batteryLevelRight = [self getBatteryIconNamed:@"BatteryLevelCapR-R"];
  316 + }
  317 +
  318 + const CGFloat capBarLeftOffset = 3.0f * [batteryLevelLeft size].width;
  319 + CGFloat capBarHeight = [batteryLevelLeft size].height;
  320 + CGFloat capBarTopOffset = (([batteryOutline size].height - EXTRA_TOP_OFFSET) - capBarHeight) / 2.0;
  321 + CGFloat capBarLength = floor(percent / 7.6f); // max width is 13px
  322 + capBarLength = (capBarLength < (2 * [batteryLevelLeft size].width)) ? (2 * [batteryLevelLeft size].width) : capBarLength;
  323 +
  324 + [batteryOutline lockFocus];
  325 + [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
  326 + NSDrawThreePartImage(NSMakeRect(capBarLeftOffset, capBarTopOffset, capBarLength, capBarHeight),
  327 + batteryLevelLeft, batteryLevelMiddle, batteryLevelRight,
  328 + NO,
  329 + NSCompositeDestinationOver, //NSCompositeSourceOver,
  330 + 0.94f,
  331 + NO);
  332 + [batteryOutline unlockFocus];
  333 +
  334 + return batteryOutline;
315 335 }
316 336
317   -- (NSImage *)getBatteryIconNamed:(NSString *)iconName
  337 +- (NSImage *)getBatteryIconNamed:(NSString *)iconName {
  338 + return [m_images objectForKey:iconName];
  339 +}
  340 +
  341 +- (NSImage *)loadBatteryIconNamed:(NSString *)iconName
318 342 {
319 343 NSString *fileName = [NSString stringWithFormat:@"/System/Library/CoreServices/Menu Extras/Battery.menu/Contents/Resources/%@.pdf", iconName];
320 344 return [[NSImage alloc] initWithContentsOfFile:fileName];
321 345 }
322 346
  347 +- (void)cacheNamedImages {
  348 + // special treatment for the BatteryCharging, BatteryCharged, and BatteryEmpty images
  349 + // they need to be shifted down by 1px to be in the same position as Apple's
  350 + NSSize newSize;
  351 + NSImage *origImg = nil;
  352 +
  353 + origImg = [self loadBatteryIconNamed:@"BatteryCharging"];
  354 + newSize.width = origImg.size.width;
  355 + newSize.height = origImg.size.height + EXTRA_TOP_OFFSET;
  356 + NSImage *imgCharging = [[NSImage alloc] initWithSize:newSize];
  357 + [imgCharging lockFocus];
  358 + [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
  359 + [origImg drawInRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
  360 + fromRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
  361 + operation:NSCompositeSourceOver
  362 + fraction:1.0];
  363 + [imgCharging unlockFocus];
  364 +
  365 + origImg = [self loadBatteryIconNamed:@"BatteryCharged"];
  366 + newSize.width = origImg.size.width;
  367 + newSize.height = origImg.size.height + EXTRA_TOP_OFFSET;
  368 + NSImage *imgCharged = [[NSImage alloc] initWithSize:newSize];
  369 + [imgCharged lockFocus];
  370 + [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
  371 + [origImg drawInRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
  372 + fromRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
  373 + operation:NSCompositeSourceOver
  374 + fraction:1.0];
  375 + [imgCharged unlockFocus];
  376 +
  377 + origImg = [self loadBatteryIconNamed:@"BatteryEmpty"];
  378 + newSize.width = origImg.size.width;
  379 + newSize.height = origImg.size.height + EXTRA_TOP_OFFSET;
  380 + NSImage *imgEmpty = [[NSImage alloc] initWithSize:newSize];
  381 + [imgEmpty lockFocus];
  382 + [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
  383 + [origImg drawInRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
  384 + fromRect:NSMakeRect(0, 0, origImg.size.width, origImg.size.height)
  385 + operation:NSCompositeSourceOver
  386 + fraction:1.0];
  387 + [imgEmpty unlockFocus];
  388 +
  389 + // finally construct the dictionary from which we will retrieve the images at runtime
  390 + m_images = [NSDictionary dictionaryWithObjectsAndKeys:
  391 + imgCharging, @"BatteryCharging",
  392 + imgCharged, @"BatteryCharged",
  393 + imgEmpty, @"BatteryEmpty",
  394 + [self loadBatteryIconNamed:@"BatteryLevelCapB-L"], @"BatteryLevelCapB-L",
  395 + [self loadBatteryIconNamed:@"BatteryLevelCapB-M"], @"BatteryLevelCapB-M",
  396 + [self loadBatteryIconNamed:@"BatteryLevelCapB-R"], @"BatteryLevelCapB-R",
  397 + [self loadBatteryIconNamed:@"BatteryLevelCapR-L"], @"BatteryLevelCapR-L",
  398 + [self loadBatteryIconNamed:@"BatteryLevelCapR-M"], @"BatteryLevelCapR-M",
  399 + [self loadBatteryIconNamed:@"BatteryLevelCapR-R"], @"BatteryLevelCapR-R",
  400 + nil];
  401 +}
  402 +
323 403 - (NSImage *)imageInvertColor:(NSImage *)_image
324 404 {
325 405 NSImage *image = [_image copy];

0 comments on commit 0fe0ba1

Please sign in to comment.
Something went wrong with that request. Please try again.