1515#import < IOKit/pwr_mgt/IOPMLib.h>
1616#import < QuartzCore/QuartzCore.h>
1717
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+
1823// IOPS notification callback on power source change
1924static void PowerSourceChanged (void * context)
2025{
@@ -23,14 +28,23 @@ static void PowerSourceChanged(void * context)
2328 [self updateStatusItem ];
2429}
2530
31+ @interface AppDelegate () {
32+ NSDictionary *m_images;
33+ BOOL m_showParens;
34+ }
35+ - (void )cacheNamedImages ;
36+ - (NSImage *)loadBatteryIconNamed : (NSString *)iconName ;
37+ @end
38+
2639@implementation AppDelegate
2740
2841@synthesize statusItem, notifications, previousPercent;
2942
3043- (void )applicationDidFinishLaunching : (NSNotification *)aNotification
3144{
3245 self.advancedSupported = ([self getAdvancedBatteryInfo ] != nil );
33-
46+ [self cacheNamedImages ];
47+
3448 // Init notification
3549 [[NSUserNotificationCenter defaultUserNotificationCenter ] setDelegate: self ];
3650 [self loadNotificationSetting ];
@@ -84,14 +98,29 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
8498 [notificationMenu setTag: kBTRMenuNotification ];
8599 [notificationMenu setSubmenu: notificationSubmenu];
86100 [notificationMenu setHidden: self .advancedSupported && ![[NSUserDefaults standardUserDefaults ] boolForKey: @" advanced" ]];
87-
101+
102+ // Build the settings submenu
103+ NSMenu *settingsSubmenu = [[NSMenu alloc ] initWithTitle: @" Settings Menu" ];
88104 // Advanced mode menu item
89- NSMenuItem *advancedMenu = [[NSMenuItem alloc ] initWithTitle: NSLocalizedString(@" Advanced mode" , @" Advanced mode setting" ) action: @selector (toggleAdvanced: ) keyEquivalent: @" " ];
90- [advancedMenu setTag: kBTRMenuAdvanced ];
91- advancedMenu.target = self;
92- advancedMenu.state = ([[NSUserDefaults standardUserDefaults ] boolForKey: @" advanced" ]) ? NSOnState : NSOffState ;
93- [advancedMenu setHidden: !self .advancedSupported];
94-
105+ NSMenuItem *advancedSubmenuItem = [[NSMenuItem alloc ] initWithTitle: NSLocalizedString(@" Advanced mode" , @" Advanced mode setting" ) action: @selector (toggleAdvanced: ) keyEquivalent: @" " ];
106+ [advancedSubmenuItem setTag: kBTRMenuAdvanced ];
107+ advancedSubmenuItem.target = self;
108+ advancedSubmenuItem.state = ([[NSUserDefaults standardUserDefaults ] boolForKey: @" advanced" ]) ? NSOnState : NSOffState ;
109+ [advancedSubmenuItem setHidden: !self .advancedSupported];
110+ [settingsSubmenu addItem: advancedSubmenuItem];
111+ // time display control menu item
112+ NSMenuItem *timeFormatSubmenuItem = [[NSMenuItem alloc ] initWithTitle: NSLocalizedString(@" Time display with braces" , @" Time display with braces setting" ) action: @selector (toggleParentheses: ) keyEquivalent: @" " ];
113+ [timeFormatSubmenuItem setTag: kBTRMenuParentheses ];
114+ timeFormatSubmenuItem.target = self;
115+ m_showParens = [[NSUserDefaults standardUserDefaults ] boolForKey: @" parentheses" ];
116+ timeFormatSubmenuItem.state = (m_showParens) ? NSOnState : NSOffState ;
117+ [settingsSubmenu addItem: timeFormatSubmenuItem];
118+
119+ // Settings menu item
120+ NSMenuItem *settingsMenu = [[NSMenuItem alloc ] initWithTitle: NSLocalizedString(@" Settings" , @" Settings menuitem" ) action: nil keyEquivalent: @" " ];
121+ [settingsMenu setTag: kBTRMenuSettings ];
122+ [settingsMenu setSubmenu: settingsSubmenu];
123+
95124 // Updater menu
96125 NSMenuItem *updaterMenu = [[NSMenuItem alloc ] initWithTitle: NSLocalizedString(@" Checking for updates…" , @" Update menuitem" ) action: nil keyEquivalent: @" " ];
97126 [updaterMenu setTag: kBTRMenuUpdater ];
@@ -108,7 +137,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
108137
109138 [statusBarMenu addItem: startAtLoginMenu];
110139 [statusBarMenu addItem: notificationMenu];
111- [statusBarMenu addItem: advancedMenu ];
140+ [statusBarMenu addItem: settingsMenu ];
112141 [statusBarMenu addItem: [NSMenuItem separatorItem ]]; // Separator
113142
114143 [statusBarMenu addItemWithTitle: NSLocalizedString(@" Energy Saver Preferences…" , @" Open Energy Saver Preferences menuitem" ) action: @selector (openEnergySaverPreference: ) keyEquivalent: @" " ];
@@ -124,7 +153,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
124153 statusItem.highlightMode = YES ;
125154 statusItem.menu = statusBarMenu;
126155 [self updateStatusItem ];
127-
156+
128157 // Capture Power Source updates and make sure our callback is called
129158 CFRunLoopSourceRef loop = IOPSNotificationCreateRunLoopSource (PowerSourceChanged, (__bridge void *)self);
130159 CFRunLoopAddSource (CFRunLoopGetCurrent (), loop, kCFRunLoopDefaultMode );
@@ -177,7 +206,10 @@ - (void)updateStatusItem
177206 NSInteger minute = timeTilCharged % 60 ;
178207
179208 // Return the time remaining string
180- [self setStatusBarImage: [self getBatteryIconNamed: @" BatteryCharging" ] title: [NSString stringWithFormat: @" %ld :%02ld " , hour, minute]];
209+ [self setStatusBarImage: [self getBatteryIconNamed: @" BatteryCharging" ] title: [NSString stringWithFormat: @" %@ %ld :%02ld %@ " ,
210+ (m_showParens)?@" (" :@" " ,
211+ hour, minute,
212+ (m_showParens)?@" )" :@" " ]];
181213 }
182214 else
183215 {
@@ -217,8 +249,11 @@ - (void)updateStatusItem
217249 NSInteger minute = (int )timeRemaining % 3600 / 60 ;
218250
219251 // Return the time remaining string
220- [self setStatusBarImage: [self getBatteryIconPercent: self .currentPercent] title: [NSString stringWithFormat: @" %ld :%02ld " , hour, minute]];
221-
252+ [self setStatusBarImage: [self getBatteryIconPercent: self .currentPercent] title: [NSString stringWithFormat: @" %@ %ld :%02ld %@ " ,
253+ (m_showParens)?@" (" :@" " ,
254+ hour, minute,
255+ (m_showParens)?@" )" :@" " ]];
256+
222257 for (NSString *key in self.notifications )
223258 {
224259 if ([[self .notifications valueForKey: key] boolValue ] && [key intValue ] == self.currentPercent )
@@ -240,17 +275,16 @@ - (void)updateStatusItem
240275- (void )setStatusBarImage : (NSImage *)image title : (NSString *)title
241276{
242277 // Image
243- [image setTemplate: YES ];
244278 [self .statusItem setImage: image];
245279 [self .statusItem setAlternateImage: [self imageInvertColor: image]];
246-
280+
247281 // Title
248282 NSDictionary *attributedStyle = [NSDictionary dictionaryWithObjectsAndKeys:
249- // Font
250- [NSFont menuFontOfSize: 12 .5f ],
251- NSFontAttributeName ,
252- nil ];
253-
283+ // Font
284+ [NSFont menuFontOfSize: 12 .0f ],
285+ NSFontAttributeName ,
286+ nil ];
287+
254288 NSAttributedString *attributedTitle = [[NSAttributedString alloc ] initWithString: title attributes: attributedStyle];
255289 self.statusItem .attributedTitle = attributedTitle;
256290}
@@ -282,44 +316,113 @@ - (NSDictionary *)getMoreAdvancedBatteryInfo
282316
283317- (NSImage *)getBatteryIconPercent : (NSInteger )percent
284318{
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;
319+ //
320+ // Mimic Apple's original battery icon using hires artwork
321+ //
322+ NSImage *batteryOutline = [self getBatteryIconNamed: @" BatteryEmpty" ];
323+ NSImage *batteryLevelLeft = nil ;
324+ NSImage *batteryLevelMiddle = nil ;
325+ NSImage *batteryLevelRight = nil ;
326+
327+ if (percent > 15 ) {
328+ // draw black capacity bar
329+ batteryLevelLeft = [self getBatteryIconNamed: @" BatteryLevelCapB-L" ];
330+ batteryLevelMiddle = [self getBatteryIconNamed: @" BatteryLevelCapB-M" ];
331+ batteryLevelRight = [self getBatteryIconNamed: @" BatteryLevelCapB-R" ];
332+ }
333+ else {
334+ // draw red capacity bar
335+ batteryLevelLeft = [self getBatteryIconNamed: @" BatteryLevelCapR-L" ];
336+ batteryLevelMiddle = [self getBatteryIconNamed: @" BatteryLevelCapR-M" ];
337+ batteryLevelRight = [self getBatteryIconNamed: @" BatteryLevelCapR-R" ];
338+ }
339+
340+ const CGFloat drawingUnit = [batteryLevelLeft size ].width ;
341+ const CGFloat capBarLeftOffset = 3 .0f * drawingUnit;
342+ CGFloat capBarHeight = [batteryLevelLeft size ].height ;
343+ CGFloat capBarTopOffset = (([batteryOutline size ].height - (EXTRA_TOP_OFFSET * drawingUnit)) - capBarHeight) / 2.0 ;
344+ CGFloat capBarLength = ceil (percent / 8 .0f ) * drawingUnit; // max width is 13 units
345+ if (capBarLength < (2 * drawingUnit)) { capBarLength = 2 * drawingUnit; }
346+
347+ [batteryOutline lockFocus ];
348+ [[NSGraphicsContext currentContext ] setImageInterpolation: NSImageInterpolationHigh];
349+ NSDrawThreePartImage (NSMakeRect (capBarLeftOffset, capBarTopOffset, capBarLength, capBarHeight),
350+ batteryLevelLeft, batteryLevelMiddle, batteryLevelRight,
351+ NO ,
352+ NSCompositeCopy ,
353+ 0 .94f ,
354+ NO );
355+ [batteryOutline unlockFocus ];
356+
357+ return batteryOutline;
358+ }
359+
360+ - (NSImage *)getBatteryIconNamed : (NSString *)iconName {
361+ return [m_images objectForKey: iconName];
315362}
316363
317- - (NSImage *)getBatteryIconNamed : (NSString *)iconName
364+ - (NSImage *)loadBatteryIconNamed : (NSString *)iconName
318365{
319366 NSString *fileName = [NSString stringWithFormat: @" /System/Library/CoreServices/Menu Extras/Battery.menu/Contents/Resources/%@ .pdf" , iconName];
320367 return [[NSImage alloc ] initWithContentsOfFile: fileName];
321368}
322369
370+ - (void )cacheNamedImages {
371+ // special treatment for the BatteryCharging, BatteryCharged, and BatteryEmpty images
372+ // they need to be shifted down by 1px to be in the same position as Apple's
373+ NSSize newSize;
374+ NSImage *origImg = nil ;
375+
376+ origImg = [self loadBatteryIconNamed: @" BatteryCharging" ];
377+ newSize.width = origImg.size .width ;
378+ newSize.height = origImg.size .height + EXTRA_TOP_OFFSET;
379+ NSImage *imgCharging = [[NSImage alloc ] initWithSize: newSize];
380+ [imgCharging lockFocus ];
381+ [[NSGraphicsContext currentContext ] setImageInterpolation: NSImageInterpolationHigh];
382+ [origImg drawInRect: NSMakeRect (0 , 0 , origImg.size.width, origImg.size.height)
383+ fromRect: NSMakeRect (0 , 0 , origImg.size.width, origImg.size.height)
384+ operation: NSCompositeSourceOver
385+ fraction: 1.0 ];
386+ [imgCharging unlockFocus ];
387+
388+ origImg = [self loadBatteryIconNamed: @" BatteryCharged" ];
389+ newSize.width = origImg.size .width ;
390+ newSize.height = origImg.size .height + EXTRA_TOP_OFFSET;
391+ NSImage *imgCharged = [[NSImage alloc ] initWithSize: newSize];
392+ [imgCharged lockFocus ];
393+ [[NSGraphicsContext currentContext ] setImageInterpolation: NSImageInterpolationHigh];
394+ [origImg drawInRect: NSMakeRect (0 , 0 , origImg.size.width, origImg.size.height)
395+ fromRect: NSMakeRect (0 , 0 , origImg.size.width, origImg.size.height)
396+ operation: NSCompositeSourceOver
397+ fraction: 1.0 ];
398+ [imgCharged unlockFocus ];
399+
400+ origImg = [self loadBatteryIconNamed: @" BatteryEmpty" ];
401+ newSize.width = origImg.size .width ;
402+ newSize.height = origImg.size .height + EXTRA_TOP_OFFSET;
403+ NSImage *imgEmpty = [[NSImage alloc ] initWithSize: newSize];
404+ [imgEmpty lockFocus ];
405+ [[NSGraphicsContext currentContext ] setImageInterpolation: NSImageInterpolationHigh];
406+ [origImg drawInRect: NSMakeRect (0 , 0 , origImg.size.width, origImg.size.height)
407+ fromRect: NSMakeRect (0 , 0 , origImg.size.width, origImg.size.height)
408+ operation: NSCompositeSourceOver
409+ fraction: 1.0 ];
410+ [imgEmpty unlockFocus ];
411+
412+ // finally construct the dictionary from which we will retrieve the images at runtime
413+ m_images = [NSDictionary dictionaryWithObjectsAndKeys:
414+ imgCharging, @" BatteryCharging" ,
415+ imgCharged, @" BatteryCharged" ,
416+ imgEmpty, @" BatteryEmpty" ,
417+ [self loadBatteryIconNamed: @" BatteryLevelCapB-L" ], @" BatteryLevelCapB-L" ,
418+ [self loadBatteryIconNamed: @" BatteryLevelCapB-M" ], @" BatteryLevelCapB-M" ,
419+ [self loadBatteryIconNamed: @" BatteryLevelCapB-R" ], @" BatteryLevelCapB-R" ,
420+ [self loadBatteryIconNamed: @" BatteryLevelCapR-L" ], @" BatteryLevelCapR-L" ,
421+ [self loadBatteryIconNamed: @" BatteryLevelCapR-M" ], @" BatteryLevelCapR-M" ,
422+ [self loadBatteryIconNamed: @" BatteryLevelCapR-R" ], @" BatteryLevelCapR-R" ,
423+ nil ];
424+ }
425+
323426- (NSImage *)imageInvertColor : (NSImage *)_image
324427{
325428 NSImage *image = [_image copy ];
@@ -363,18 +466,19 @@ - (void)toggleStartAtLogin:(id)sender
363466
364467- (void )toggleAdvanced : (id )sender
365468{
469+ NSMenuItem *item = sender;
366470 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults ];
367471
368472 if ([defaults boolForKey: @" advanced" ])
369473 {
370- [ self .statusItem.menu itemWithTag: kBTRMenuAdvanced ] .state = NSOffState ;
474+ item .state = NSOffState ;
371475 [[self .statusItem.menu itemWithTag: kBTRMenuPowerSourceAdvanced ] setHidden: YES ];
372476 [[self .statusItem.menu itemWithTag: kBTRMenuNotification ] setHidden: YES ];
373477 [defaults setBool: NO forKey: @" advanced" ];
374478 }
375479 else
376480 {
377- [ self .statusItem.menu itemWithTag: kBTRMenuAdvanced ] .state = NSOnState ;
481+ item .state = NSOnState ;
378482 [[self .statusItem.menu itemWithTag: kBTRMenuPowerSourceAdvanced ] setHidden: NO ];
379483 [[self .statusItem.menu itemWithTag: kBTRMenuNotification ] setHidden: NO ];
380484 [defaults setBool: YES forKey: @" advanced" ];
@@ -384,6 +488,28 @@ - (void)toggleAdvanced:(id)sender
384488 [self updateStatusItem ];
385489}
386490
491+ - (void )toggleParentheses : (id )sender
492+ {
493+ NSMenuItem *item = sender;
494+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults ];
495+
496+ if ([defaults boolForKey: @" parentheses" ])
497+ {
498+ item.state = NSOffState ;
499+ m_showParens = NO ;
500+ [defaults setBool: NO forKey: @" parentheses" ];
501+ }
502+ else
503+ {
504+ item.state = NSOnState ;
505+ m_showParens = YES ;
506+ [defaults setBool: YES forKey: @" parentheses" ];
507+ }
508+ [defaults synchronize ];
509+
510+ [self updateStatusItem ];
511+ }
512+
387513- (void )notify : (NSString *)message
388514{
389515 NSUserNotification *notification = [[NSUserNotification alloc ] init ];
@@ -452,7 +578,7 @@ - (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNot
452578- (void )menuWillOpen : (NSMenu *)menu
453579{
454580 // Show power source data in menu
455- if (self.advancedSupported && [self .statusItem.menu itemWithTag: kBTRMenuAdvanced ].state == NSOnState )
581+ if (self.advancedSupported && [[ self .statusItem.menu itemWithTag: kBTRMenuSettings ].submenu itemWithTag: kBTRMenuAdvanced ].state == NSOnState )
456582 {
457583 NSDictionary *advancedBatteryInfo = [self getAdvancedBatteryInfo ];
458584 NSDictionary *moreAdvancedBatteryInfo = [self getMoreAdvancedBatteryInfo ];
0 commit comments