Skip to content

Commit 3b90878

Browse files
committed
Merge pull request codler#19 from c-alpha/master
Updated look & feel
2 parents d99cce6 + 9b1a618 commit 3b90878

File tree

7 files changed

+182
-54
lines changed

7 files changed

+182
-54
lines changed

Battery Time Remaining/AppDelegate.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#define kBTRMenuEnergySaverSetting 7
2121
#define kBTRMenuUpdater 8
2222
#define kBTRMenuQuitKey 9
23+
#define kBTRMenuSettings 10
24+
#define kBTRMenuParentheses 11
2325

2426
#endif
2527

Battery Time Remaining/AppDelegate.m

Lines changed: 180 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
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
1924
static 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];
304 Bytes
Binary file not shown.
292 Bytes
Binary file not shown.
308 Bytes
Binary file not shown.
294 Bytes
Binary file not shown.
294 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)