Skip to content

Commit

Permalink
Add support for custom Dock icons while running
Browse files Browse the repository at this point in the history
The emoji labeled buttons will convert and save their respective
state icon to the settings folder, and refresh the current icon
as necessary.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
  • Loading branch information
kode54 committed Nov 25, 2024
1 parent aa491fe commit f5be598
Show file tree
Hide file tree
Showing 18 changed files with 305 additions and 27 deletions.
7 changes: 7 additions & 0 deletions Application/DockIconController.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
@interface DockIconController : NSObject {
NSImage *dockImage;

NSInteger lastDockCustom;
NSInteger lastDockCustomPlaque;
NSInteger dockCustomLoaded;
NSImage *dockCustomStop;
NSImage *dockCustomPlay;
NSImage *dockCustomPause;

IBOutlet PlaybackController *playbackController;

NSInteger lastPlaybackStatus;
Expand Down
78 changes: 73 additions & 5 deletions Application/DockIconController.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,24 @@ @implementation DockIconController

static NSString *DockIconPlaybackStatusObservationContext = @"DockIconPlaybackStatusObservationContext";

static NSString *CogCustomDockIconsReloadNotification = @"CogCustomDockIconsReloadNotification";

- (void)startObserving {
[playbackController addObserver:self forKeyPath:@"playbackStatus" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[playbackController addObserver:self forKeyPath:@"progressOverall" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld) context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.colorfulDockIcons" options:0 context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.customDockIcons" options:0 context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.customDockIconsPlaque" options:0 context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshDockIcons:) name:CogCustomDockIconsReloadNotification object:nil];
}

- (void)stopObserving {
[playbackController removeObserver:self forKeyPath:@"playbackStatus" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[playbackController removeObserver:self forKeyPath:@"progressOverall" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.colorfulDockIcons" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.customDockIcons" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.customDockIconsPlaque" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSNotificationCenter defaultCenter] removeObserver:self name:CogCustomDockIconsReloadNotification object:nil];
}

- (void)startObservingProgress:(NSProgress *)progress {
Expand All @@ -42,6 +50,34 @@ - (void)stopObservingProgress:(NSProgress *)progress {
}
}

static NSString *getCustomIconName(NSString *baseName) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString *basePath = [[paths firstObject] stringByAppendingPathComponent:@"Cog"];
basePath = [basePath stringByAppendingPathComponent:@"Icons"];
basePath = [basePath stringByAppendingPathComponent:baseName];
return [basePath stringByAppendingPathExtension:@"png"];
}

- (BOOL)loadCustomDockIcons {
NSError *error = nil;
NSData *dataStopIcon = [NSData dataWithContentsOfFile:getCustomIconName(@"Stop") options:(NSDataReadingMappedIfSafe) error:&error];
if(!dataStopIcon || error) {
return NO;
}
NSData *dataPlayIcon = [NSData dataWithContentsOfFile:getCustomIconName(@"Play") options:(NSDataReadingMappedIfSafe) error:&error];
if(!dataPlayIcon || error) {
return NO;
}
NSData *dataPauseIcon = [NSData dataWithContentsOfFile:getCustomIconName(@"Pause") options:(NSDataReadingMappedIfSafe) error:&error];
if(!dataPauseIcon || error) {
return NO;
}
dockCustomStop = [[NSImage alloc] initWithData:dataStopIcon];
dockCustomPlay = [[NSImage alloc] initWithData:dataPlayIcon];
dockCustomPause = [[NSImage alloc] initWithData:dataPauseIcon];
return (dockCustomStop && dockCustomPlay && dockCustomPause);
}

- (void)refreshDockIcon:(NSInteger)playbackStatus withProgress:(double)progressStatus {
// Really weird crash user experienced because the plaque image didn't load?
if(!dockImage || dockImage.size.width == 0 || dockImage.size.height == 0) return;
Expand All @@ -50,6 +86,30 @@ - (void)refreshDockIcon:(NSInteger)playbackStatus withProgress:(double)progressS
BOOL drawIcon = NO;
BOOL removeProgress = NO;

BOOL useCustomDockIcons = [[NSUserDefaults standardUserDefaults] boolForKey:@"customDockIcons"];
BOOL useCustomDockIconsPlaque = [[NSUserDefaults standardUserDefaults] boolForKey:@"customDockIconsPlaque"];

if(useCustomDockIcons && !dockCustomLoaded) {
dockCustomLoaded = [self loadCustomDockIcons];
if(!dockCustomLoaded) {
useCustomDockIcons = NO;
}
}

if(useCustomDockIcons != lastDockCustom ||
useCustomDockIconsPlaque != lastDockCustomPlaque) {
lastDockCustom = useCustomDockIcons;
lastDockCustomPlaque = useCustomDockIconsPlaque;
drawIcon = YES;

if(!useCustomDockIcons) {
dockCustomLoaded = NO;
dockCustomStop = nil;
dockCustomPlay = nil;
dockCustomPause = nil;
}
}

if(playbackStatus < 0)
playbackStatus = lastPlaybackStatus;
else {
Expand Down Expand Up @@ -82,20 +142,20 @@ - (void)refreshDockIcon:(NSInteger)playbackStatus withProgress:(double)progressS
if(drawIcon) {
switch(playbackStatus) {
case CogStatusPlaying:
badgeImage = [NSImage imageNamed:getBadgeName(@"Play", colorfulIcons)];
badgeImage = useCustomDockIcons ? dockCustomPlay : [NSImage imageNamed:getBadgeName(@"Play", colorfulIcons)];
break;
case CogStatusPaused:
badgeImage = [NSImage imageNamed:getBadgeName(@"Pause", colorfulIcons)];
badgeImage = useCustomDockIcons ? dockCustomPause : [NSImage imageNamed:getBadgeName(@"Pause", colorfulIcons)];
break;

default:
badgeImage = [NSImage imageNamed:getBadgeName(@"Stop", colorfulIcons)];
badgeImage = useCustomDockIcons ? dockCustomStop : [NSImage imageNamed:getBadgeName(@"Stop", colorfulIcons)];
break;
}

NSSize badgeSize = [badgeImage size];

NSImage *newDockImage = [dockImage copy];
NSImage *newDockImage = (useCustomDockIcons && !useCustomDockIconsPlaque) ? [[NSImage alloc] initWithSize:NSMakeSize(1024, 1024)] : [dockImage copy];
[newDockImage lockFocus];

[badgeImage drawInRect:NSMakeRect(0, 0, 1024, 1024)
Expand Down Expand Up @@ -186,7 +246,9 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
}

[self refreshDockIcon:-1 withProgress:progressStatus];
} else if([keyPath isEqualToString:@"values.colorfulDockIcons"]) {
} else if([keyPath isEqualToString:@"values.colorfulDockIcons"] ||
[keyPath isEqualToString:@"values.customDockIcons"] ||
[keyPath isEqualToString:@"values.customDockIconsPlaque"]) {
[self refreshDockIcon:-1 withProgress:-10];
} else if([keyPath isEqualToString:@"fractionCompleted"]) {
double progressStatus = [(NSProgress *)object fractionCompleted];
Expand All @@ -197,6 +259,12 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
}
}

- (void)refreshDockIcons:(NSNotification *)notification {
lastDockCustom = NO;
dockCustomLoaded = NO;
[self refreshDockIcon:-1 withProgress:-10];
}

- (void)awakeFromNib {
dockImage = [[NSImage imageNamed:@"Plaque"] copy];
lastColorfulStatus = -1;
Expand Down
19 changes: 19 additions & 0 deletions Preferences/Preferences/AppearancePane.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// AppearancePane.h
// General
//
// Created by Christopher Snowhill on 11/24/24.
//
//

#import "GeneralPreferencePane.h"
#import <Cocoa/Cocoa.h>

@interface AppearancePane : GeneralPreferencePane {
}

- (IBAction)setDockIconStop:(id)sender;
- (IBAction)setDockIconPlay:(id)sender;
- (IBAction)setDockIconPause:(id)sender;

@end
90 changes: 90 additions & 0 deletions Preferences/Preferences/AppearancePane.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// AppearancePane.m
// General
//
// Created by Christopher Snowhill on 11/24/24.
//
//

#import "AppearancePane.h"

static NSString *CogCustomDockIconsReloadNotification = @"CogCustomDockIconsReloadNotification";

@implementation AppearancePane

- (NSString *)title {
return NSLocalizedPrefString(@"Appearance");
}

- (NSImage *)icon {
if(@available(macOS 11.0, *))
return [NSImage imageWithSystemSymbolName:@"paintpalette.fill" accessibilityDescription:nil];
return [[NSImage alloc] initWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForImageResource:@"appearance"]];
}

- (void)setDockIcon:(NSString *)baseName {
NSArray *fileTypes = @[@"jpg", @"jpeg", @"png", @"gif", @"webp", @"avif", @"heic"];
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:NO];
[panel setCanChooseFiles:YES];
[panel setFloatingPanel:YES];
[panel setAllowedFileTypes:fileTypes];
NSInteger result = [panel runModal];
if(result == NSModalResponseOK) {
NSError *error = nil;
NSData *iconData = [NSData dataWithContentsOfURL:[panel URL] options:NSDataReadingMappedIfSafe error:&error];
if(iconData && !error) {
NSImage *icon = [[NSImage alloc] initWithData:iconData];
if(icon) {
CGImageRef cgRef = [icon CGImageForProposedRect:NULL
context:nil
hints:nil];

if(cgRef) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString *basePath = [[paths firstObject] stringByAppendingPathComponent:@"Cog"];
basePath = [basePath stringByAppendingPathComponent:@"Icons"];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory = NO;
if(![fileManager fileExistsAtPath:basePath isDirectory:&isDirectory] || !isDirectory) {
if(!isDirectory) {
[fileManager removeItemAtPath:basePath error:&error];
}
[fileManager createDirectoryAtURL:[NSURL fileURLWithPath:basePath] withIntermediateDirectories:YES attributes:nil error:&error];
}

NSString *filePath = [basePath stringByAppendingPathComponent:baseName];
filePath = [filePath stringByAppendingPathExtension:@"png"];

NSBitmapImageRep *newRep =
[[NSBitmapImageRep alloc] initWithCGImage:cgRef];
NSData *pngData = [newRep
representationUsingType:NSBitmapImageFileTypePNG
properties:@{}];
[pngData writeToURL:[NSURL fileURLWithPath:filePath] atomically:YES];

// Now to refresh the icons by a little trickery, if enabled
BOOL enabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"customDockIcons"];
if(enabled) {
[[NSNotificationCenter defaultCenter] postNotificationName:CogCustomDockIconsReloadNotification object:nil];
}
}
}
}
}
}

- (IBAction)setDockIconStop:(id)sender {
[self setDockIcon:@"Stop"];
}

- (IBAction)setDockIconPlay:(id)sender {
[self setDockIcon:@"Play"];
}

- (IBAction)setDockIconPause:(id)sender {
[self setDockIcon:@"Pause"];
}

@end
Loading

0 comments on commit f5be598

Please sign in to comment.