diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index c44dcfb..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 89209b5..4984fab 100644 --- a/.gitignore +++ b/.gitignore @@ -30,39 +30,5 @@ DerivedData/ *.dSYM.zip *.dSYM -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ \ No newline at end of file +# macOS nonsense +*.DS_Store diff --git a/ObjC/.DS_Store b/ObjC/.DS_Store deleted file mode 100644 index 0f4cd1e..0000000 Binary files a/ObjC/.DS_Store and /dev/null differ diff --git a/ObjC/Base.lproj/.DS_Store b/ObjC/Base.lproj/.DS_Store deleted file mode 100644 index bc8d1c0..0000000 Binary files a/ObjC/Base.lproj/.DS_Store and /dev/null differ diff --git a/ObjC/DockTile.ObjC-Info.plist b/ObjC/DockTile.ObjC-Info.plist index e1eb138..ea3ea1b 100644 --- a/ObjC/DockTile.ObjC-Info.plist +++ b/ObjC/DockTile.ObjC-Info.plist @@ -16,10 +16,12 @@ ${PRODUCT_NAME} CFBundlePackageType APPL + CFBundleShortVersionString + 1.2 CFBundleSignature ???? CFBundleVersion - 1.0 + 3 LSMinimumSystemVersion 10.6 NSDockTilePlugIn @@ -28,5 +30,20 @@ MainMenu NSPrincipalClass NSApplication + DockIconsToChooseFrom + + + title + Classic Icon + file name + icon.icns + + + title + Squircle Icon + file name + icon2.icns + + diff --git a/ObjC/DockTile.ObjC.xcodeproj/project.pbxproj b/ObjC/DockTile.ObjC.xcodeproj/project.pbxproj index 1ef5727..a79104b 100644 --- a/ObjC/DockTile.ObjC.xcodeproj/project.pbxproj +++ b/ObjC/DockTile.ObjC.xcodeproj/project.pbxproj @@ -16,6 +16,11 @@ 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; + A42C411A26BD4A260055F67C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A42C411926BD4A260055F67C /* Foundation.framework */; }; + A42C411C26BD4A370055F67C /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A42C411B26BD4A370055F67C /* AppKit.framework */; }; + A42C412326BD4F1D0055F67C /* icon2.icns in Resources */ = {isa = PBXBuildFile; fileRef = A42C412226BD4F1D0055F67C /* icon2.icns */; }; + A42C412626BD4F5A0055F67C /* SharedDockItem.m in Sources */ = {isa = PBXBuildFile; fileRef = A42C412426BD4F5A0055F67C /* SharedDockItem.m */; }; + A42C412726BD4F5A0055F67C /* SharedDockItem.m in Sources */ = {isa = PBXBuildFile; fileRef = A42C412426BD4F5A0055F67C /* SharedDockItem.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,6 +65,11 @@ 9673543E23A9035700FD6C03 /* Base */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = Base; path = Base.lproj/MainMenu.nib; sourceTree = ""; }; 9673543F23A9035D00FD6C03 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 9673544023A9035D00FD6C03 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + A42C411926BD4A260055F67C /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + A42C411B26BD4A370055F67C /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + A42C412226BD4F1D0055F67C /* icon2.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = icon2.icns; sourceTree = ""; }; + A42C412426BD4F5A0055F67C /* SharedDockItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SharedDockItem.m; path = DockTilePlugIn/SharedDockItem.m; sourceTree = SOURCE_ROOT; }; + A42C412526BD4F5A0055F67C /* SharedDockItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SharedDockItem.h; path = DockTilePlugIn/SharedDockItem.h; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,6 +77,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A42C411C26BD4A370055F67C /* AppKit.framework in Frameworks */, + A42C411A26BD4A260055F67C /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -112,10 +124,11 @@ children = ( 41D5F1550FEE95CE004546AB /* README.rtf */, 29B97315FDCFA39411CA2CEA /* Sources */, + 53D6795A0FD6EF91001688B3 /* DockTilePlugin */, + A42C411D26BD4EFB0055F67C /* Shared */, 29B97317FDCFA39411CA2CEA /* Resources */, 29B97323FDCFA39411CA2CEA /* Frameworks */, 19C28FACFE9D520D11CA2CBB /* Products */, - 53D6795A0FD6EF91001688B3 /* DockTilePlugin */, ); name = TrackIt; sourceTree = ""; @@ -135,6 +148,7 @@ isa = PBXGroup; children = ( 53FE44180FD71991007FBB65 /* icon.icns */, + A42C412226BD4F1D0055F67C /* icon2.icns */, 536F14110D33E1A200B2806D /* Credits.rtf */, 8D1107310486CEB800E47090 /* DockTile.ObjC-Info.plist */, 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, @@ -146,6 +160,8 @@ 29B97323FDCFA39411CA2CEA /* Frameworks */ = { isa = PBXGroup; children = ( + A42C411B26BD4A370055F67C /* AppKit.framework */, + A42C411926BD4A260055F67C /* Foundation.framework */, 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, ); @@ -162,6 +178,15 @@ name = DockTilePlugin; sourceTree = ""; }; + A42C411D26BD4EFB0055F67C /* Shared */ = { + isa = PBXGroup; + children = ( + A42C412526BD4F5A0055F67C /* SharedDockItem.h */, + A42C412426BD4F5A0055F67C /* SharedDockItem.m */, + ); + path = Shared; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -244,6 +269,7 @@ 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, 536F14130D33E1A200B2806D /* Credits.rtf in Resources */, 53FE44190FD71991007FBB65 /* icon.icns in Resources */, + A42C412326BD4F1D0055F67C /* icon2.icns in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -254,6 +280,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A42C412726BD4F5A0055F67C /* SharedDockItem.m in Sources */, 53D67A8F0FD6F773001688B3 /* DockTilePlugIn.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -264,6 +291,7 @@ files = ( 8D11072D0486CEB800E47090 /* main.m in Sources */, 53CC47720ABB251700F70973 /* DockTileAppDelegate.m in Sources */, + A42C412626BD4F5A0055F67C /* SharedDockItem.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -308,24 +336,15 @@ 53D679D70FD6F0DC001688B3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ENABLE_OBJC_WEAK = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; GCC_DYNAMIC_NO_PIC = NO; - GCC_MODEL_TUNING = G5; GCC_OPTIMIZATION_LEVEL = 0; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h"; INFOPLIST_FILE = "DockTilePlugIn/DockTile.ObjC.PlugIn-Info.plist"; INSTALL_PATH = "$(LOCAL_DEVELOPER_DIR)/Demos"; - MACOSX_DEPLOYMENT_TARGET = 10.9; - OTHER_LDFLAGS = ( - "-framework", - Foundation, - "-framework", - AppKit, - ); PRODUCT_BUNDLE_IDENTIFIER = com.apple.samplecode.DockTilePlugIn; PRODUCT_NAME = DockTile.ObjC; WRAPPER_EXTENSION = docktileplugin; @@ -335,22 +354,13 @@ 53D679D80FD6F0DC001688B3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ENABLE_OBJC_WEAK = YES; COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - GCC_MODEL_TUNING = G5; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h"; INFOPLIST_FILE = "DockTilePlugIn/DockTile.ObjC.PlugIn-Info.plist"; INSTALL_PATH = "$(LOCAL_DEVELOPER_DIR)/Demos"; - MACOSX_DEPLOYMENT_TARGET = 10.9; - OTHER_LDFLAGS = ( - "-framework", - Foundation, - "-framework", - AppKit, - ); PRODUCT_BUNDLE_IDENTIFIER = com.apple.samplecode.DockTilePlugIn; PRODUCT_NAME = DockTile.ObjC; WRAPPER_EXTENSION = docktileplugin; @@ -366,12 +376,10 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; GCC_DYNAMIC_NO_PIC = NO; - GCC_MODEL_TUNING = G5; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREFIX_HEADER = Prefix.pch; INFOPLIST_FILE = "DockTile.ObjC-Info.plist"; INSTALL_PATH = "$(LOCAL_DEVELOPER_DIR)/Demos"; - MACOSX_DEPLOYMENT_TARGET = 10.9; PRODUCT_BUNDLE_IDENTIFIER = com.apple.examples.DockTile.ObjC.App; PRODUCT_NAME = DockTile.ObjC; WRAPPER_EXTENSION = app; @@ -386,11 +394,9 @@ CODE_SIGN_IDENTITY = "-"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; - GCC_MODEL_TUNING = G5; GCC_PREFIX_HEADER = Prefix.pch; INFOPLIST_FILE = "DockTile.ObjC-Info.plist"; INSTALL_PATH = "$(LOCAL_DEVELOPER_DIR)/Demos"; - MACOSX_DEPLOYMENT_TARGET = 10.9; PRODUCT_BUNDLE_IDENTIFIER = com.apple.examples.DockTile.ObjC.App; PRODUCT_NAME = DockTile.ObjC; WRAPPER_EXTENSION = app; @@ -400,7 +406,9 @@ C01FCF4F08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; @@ -421,13 +429,14 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_NO_COMMON_BLOCKS = YES; + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1"; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.6; + MACOSX_DEPLOYMENT_TARGET = 10.8; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; }; @@ -436,7 +445,9 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; @@ -462,7 +473,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.6; + MACOSX_DEPLOYMENT_TARGET = 10.8; SDKROOT = macosx; }; name = Release; diff --git a/ObjC/DockTileAppDelegate.h b/ObjC/DockTileAppDelegate.h index 85969ad..1ab155c 100644 --- a/ObjC/DockTileAppDelegate.h +++ b/ObjC/DockTileAppDelegate.h @@ -53,10 +53,11 @@ */ -#include +#import @interface DockTileAppDelegate : NSObject{ NSMenu *dockMenu; + NSWindow *window; } @property NSInteger highScore; diff --git a/ObjC/DockTileAppDelegate.m b/ObjC/DockTileAppDelegate.m index c0bd564..fbfc67d 100644 --- a/ObjC/DockTileAppDelegate.m +++ b/ObjC/DockTileAppDelegate.m @@ -1,5 +1,5 @@ /* - File: DockTileAppDelegate.m + File: DockTileAppDelegate.m Abstract: DockTile is a "game" which demonstrates the use of NSDockTile, and more importantly, the NSDockTilePlugIn protocol introduced in 10.6. The game is terribly simple: Your score goes up by 1 just by launching the app! So keep on launching the app over and over to reach new high scores. @@ -9,7 +9,8 @@ The whole game is implemented in the DockTileAppDelegate class, which is the delegate of the application. On applicationDidFinishLaunching: it updates the highScore. The only other thing it does is to implement resetHighScore: to set it back to 0. The dock tile plug-in is useful as a way to show off your high score even when the app is not running. The plug-in simply reads the high score from defaults, displays it as a badge on the dock tile, then updates it on receipt of a distributed notification that indicates when the high score changed. - Version: 1.1 + + Version: 1.2 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following @@ -53,65 +54,93 @@ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF */ -#include "DockTileAppDelegate.h" +#import "DockTileAppDelegate.h" +#import "SharedDockItem.h" + +// A convenience macro to make the "not" operator more visible: +#define NOT ! @implementation DockTileAppDelegate +-(instancetype)init { + self = [super init]; + if (self) { + // + // We want to update the dock icon as soon as possible, hence we do it here in the init method + // + + // Determine the icon to show + NSString *dockIconName = [[NSUserDefaults standardUserDefaults] objectForKey:PrefsKeyDockIcon]; + NSImage *icon = [NSBundle.mainBundle imageForResource:dockIconName]; + if (dockIconName == nil || icon == nil) { + if (@available(macOS 11.0, *)) { + dockIconName = @"icon2.icns"; // let's default to using the alternative (squircle) icon on Big Sur and later + } else { + dockIconName = DEFAULT_ICON_NAME; + } + [[NSUserDefaults standardUserDefaults] setObject:dockIconName forKey:PrefsKeyDockIcon]; + [NSDistributedNotificationCenter.defaultCenter postNotificationName:DockUpdateNotificationKey object:nil]; + icon = [NSBundle.mainBundle imageForResource:dockIconName]; + } + + // Now change the icon in the Dock (this is the part that needs to be done ASAP) + if (NOT [dockIconName isEqualToString:@"icon.icns"]) { // no need to change it if it's the default app icon + [NSApp setApplicationIconImage:icon]; // required on macOS 11 or the app icon won't update for a few seconds + } + + // Finally, observe changes to the dock, e.g. when the user chose a different icon via our custom Dock Tile menu + [NSDistributedNotificationCenter.defaultCenter addObserverForName:DockUpdateNotificationKey object:nil queue:nil usingBlock:^(NSNotification *notification) { + NSString *dockIconName2 = [[NSUserDefaults standardUserDefaults] objectForKey:PrefsKeyDockIcon]; + NSImage *icon2 = [NSBundle.mainBundle imageForResource:dockIconName2]; + if (icon2) { + [NSApp setApplicationIconImage:icon2]; + } + }]; + } + return self; +} + /* highScore accessors. highScore is declared as a property, but we don't have an instance variable for it, and nor do we use synthesized accessors since we do special things. */ - (NSInteger)highScore { - // We get the value from defaults (preferences), we don't keep a copy of the high score in the app. - return [[NSUserDefaults standardUserDefaults] integerForKey:@"HighScore"]; + // We get the value from defaults (preferences), we don't keep a copy of the high score in the app. + return [[NSUserDefaults standardUserDefaults] integerForKey:PrefsKeyHighScore]; } - (void)setHighScore:(NSInteger)newScore { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - - // We just save the value out, we don't keep a copy of the high score in the app. - [defaults setInteger:newScore forKey:@"HighScore"]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + // We just save the value out, we don't keep a copy of the high score in the app. + [defaults setInteger:newScore forKey:PrefsKeyHighScore]; - // Save the value out to defaults now. We often don't explicit synchronize, since it's best to let the system take care of it automatically. However, in this case since we're asking the plug-in to update the score, synchronizing before the notification ensures that the plug-in sees the latest value. Always make sure the value is updated and synchronized before sending out the distributed notification to other processes. - [defaults synchronize]; + // Save the value out to defaults now. We often don't explicit synchronize, since it's best to let the system take care of it automatically. However, in this case since we're asking the plug-in to update the score, synchronizing before the notification ensures that the plug-in sees the latest value. Always make sure the value is updated and synchronized before sending out the distributed notification to other processes. + [defaults synchronize]; - // And post a notification so the plug-in sees the change. - [[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.apple.DockTile.ObjC.HighScoreChanged" object:nil]; + // And post a notification so the plug-in sees the change. + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:DockUpdateNotificationKey object:nil]; - // Now update the dock tile. Note that a more general way to do this would be to observe the highScore property, but we're just keeping things short and sweet here, trying to demo how to write a plug-in. + // Now update the dock tile's badge. + // Note that a more general way to do this would be to observe the highScore property, but we're just keeping things short and sweet here, trying to demo how to write a plug-in. [[[NSApplication sharedApplication] dockTile] setBadgeLabel:[NSString stringWithFormat:@"%ld", (long)newScore]]; } /* On launch, get the previous score, increment by one, and save it. By definition all updated scores are high scores. */ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - [self setHighScore:[self highScore] + 1]; + [self setHighScore:[self highScore] + 1]; } /* Reset the high score. Simple... */ - (void)resetHighScore:(id)sender { - [self setHighScore:0]; + [self setHighScore:0]; } -// This menu will ONLY appear when the app is running. We need NSDockPlugin for it to appear when the app isn't running. -- (NSMenu *)applicationDockMenu:(NSApplication *)sender { - // Create the menu - if (dockMenu == nil) - dockMenu = [[NSMenu alloc] init]; - else - [dockMenu removeAllItems]; - - // Let's Find the HighScore - CFPreferencesAppSynchronize(CFSTR("com.apple.examples.DockTile.ObjC.App")); - NSInteger highScore = CFPreferencesGetAppIntegerValue(CFSTR("HighScore"), CFSTR("com.apple.examples.DockTile.ObjC.App"), NULL); - - // Convert it into a string - NSString *highScoreAsString = [NSString stringWithFormat:@"%ld", (long)highScore]; - NSMenuItem *highScoreMenu = [[NSMenuItem alloc] initWithTitle:highScoreAsString action:NULL keyEquivalent:@""]; - - [dockMenu addItem: highScoreMenu]; - [highScoreMenu release]; - - return dockMenu; +/* This menu will ONLY appear when the app is running. We need NSDockPlugin for it to appear when the app isn't running. +*/ +- (NSMenu *)applicationDockMenu:(NSApplication *)sender +{ + return [SharedDockItem dockMenuForPrefs:[NSUserDefaults standardUserDefaults] bundle:[NSBundle mainBundle]]; } @end diff --git a/ObjC/DockTilePlugIn/DockTile.ObjC.PlugIn-Info.plist b/ObjC/DockTilePlugIn/DockTile.ObjC.PlugIn-Info.plist index 7c8e5c4..2cc3f1b 100644 --- a/ObjC/DockTilePlugIn/DockTile.ObjC.PlugIn-Info.plist +++ b/ObjC/DockTilePlugIn/DockTile.ObjC.PlugIn-Info.plist @@ -13,11 +13,11 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.0 + 1.2 CFBundleSignature ???? CFBundleVersion - 1 + 3 NSPrincipalClass DockTilePlugIn diff --git a/ObjC/DockTilePlugIn/DockTilePlugIn.h b/ObjC/DockTilePlugIn/DockTilePlugIn.h index a272e53..3cd285e 100644 --- a/ObjC/DockTilePlugIn/DockTilePlugIn.h +++ b/ObjC/DockTilePlugIn/DockTilePlugIn.h @@ -53,12 +53,15 @@ */ -#include +#import -@interface DockTilePlugIn : NSObject { - id highScoreObserver; - NSMenu *dockMenu; -} +@interface DockTilePlugIn : NSObject + +@property(strong) id updateObserver; +@property(strong) NSBundle *mainBundle; +@property(strong) NSUserDefaults *mainPrefs; +@property(strong) NSString *currentIconName; + +- (NSMenu*)dockMenu; // conforms to NSDockTilePlugIn protocol -@property(retain) id highScoreObserver; @end diff --git a/ObjC/DockTilePlugIn/DockTilePlugIn.m b/ObjC/DockTilePlugIn/DockTilePlugIn.m index d73e416..102dcc8 100644 --- a/ObjC/DockTilePlugIn/DockTilePlugIn.m +++ b/ObjC/DockTilePlugIn/DockTilePlugIn.m @@ -1,5 +1,6 @@ /* - File: DockTilePlugIn.m + File: DockTilePlugIn.m + Abstract: DockTile is a "game" which demonstrates the use of NSDockTile, and more importantly, the NSDockTilePlugIn protocol introduced in 10.6. The game is terribly simple: Your score goes up by 1 just by launching the app! So keep on launching the app over and over to reach new high scores. @@ -9,7 +10,8 @@ When the plug-in is loaded, an instance of DockTilePlugIn is instantiated, and setDockTile: called with an instance of NSDockTile. The implementation of setDockTile: in DockTilePlugIn sets the plug-in as an observer of high score changes (using NSDistributedNotification), causing the badge on the dock tile to update everytime the score is updated. The NSDistributedNotificationCenter registry happens with the 10.6 method addObserverForName:object:queue:block:. The body of the block has no references to the DockTilePlugIn instance, which means the notification does not cause it to be retained. In this case that does not matter (since setDockTile:nil is called, which removes the observer), but in some cases this is important to watch for. - Version: 1.1 + + Version: 1.2 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following @@ -50,66 +52,88 @@ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2011 Apple Inc. All Rights Reserved. - + */ -#include "DockTilePlugIn.h" +#import "DockTilePlugIn.h" +#import "SharedDockItem.h" @implementation DockTilePlugIn -@synthesize highScoreObserver; +- (void) updateDockTile:(NSDockTile*)dockTile +{ + [self.mainPrefs synchronize]; -/* To update the score, we first make sure we are looking at the latest state of the defaults database, then we fetch the score and set it as the badge label. -*/ -static void updateScore(NSDockTile *tile) { - CFPreferencesAppSynchronize(CFSTR("com.apple.examples.DockTile.ObjC.App")); - NSInteger highScore = CFPreferencesGetAppIntegerValue(CFSTR("HighScore"), CFSTR("com.apple.examples.DockTile.ObjC.App"), NULL); - [tile setBadgeLabel:[NSString stringWithFormat:@"%ld", (long)highScore]]; + // update icon + NSString *iconName = [self.mainPrefs objectForKey:PrefsKeyDockIcon]; + if ([iconName isEqualToString:self.currentIconName]) { + // no change necessary + } else { + self.currentIconName = iconName; + NSImage *icon = [self.mainBundle imageForResource:iconName]; + if (icon.isValid) { + NSView *view = [NSView.alloc initWithFrame:NSMakeRect(0, 0, dockTile.size.width, dockTile.size.height)]; + NSImageView *iconView = [NSImageView.alloc initWithFrame:view.frame]; + iconView.image = icon; + iconView.imageScaling = NSImageScaleProportionallyDown; + [iconView setFrameSize:dockTile.size]; + [view addSubview:iconView]; + [dockTile setContentView:view]; + [dockTile display]; + } + } + + // update badge + NSInteger highScore = [self.mainPrefs integerForKey:PrefsKeyHighScore]; + [dockTile setBadgeLabel:[NSString stringWithFormat:@"%ld", (long)highScore]]; } -- (void)setDockTile:(NSDockTile *)dockTile { - if (dockTile) { - // Attach an observer that will update the high score in the dock tile whenever it changes - self.highScoreObserver = [[NSDistributedNotificationCenter defaultCenter] addObserverForName:@"com.apple.DockTile.ObjC.HighScoreChanged" object:nil queue:nil usingBlock:^(NSNotification *notification) { - updateScore(dockTile); // Note that this block captures (and retains) dockTile for use later. Also note that it does not capture self, which means -dealloc may be called even while the notification is active. Although it's not clear this needs to be supported, this does eliminate a potential source of leaks. - }]; - updateScore(dockTile); // Make sure score is updated from the get-go as well - } else { - // Strictly speaking this may not be necessary (since the plug-in may be terminated when it's removed from the dock), but it's good practice - [[NSDistributedNotificationCenter defaultCenter] removeObserver:self.highScoreObserver]; - self.highScoreObserver = nil; - } +- (void) setDockTile:(NSDockTile *)dockTile +{ + // Determine the app bundle that includes this plugin inside its "Contents/PlugIn" folder + if (dockTile != nil && (self.mainBundle == nil || self.mainPrefs == nil)) { + NSBundle *bundle = [NSBundle bundleForClass:DockTilePlugIn.class]; + bundle = [NSBundle bundleWithURL:bundle.bundleURL.URLByDeletingLastPathComponent.URLByDeletingLastPathComponent.URLByDeletingLastPathComponent]; + NSString *prefsID = [bundle objectForInfoDictionaryKey:@"CFBundleIdentifier"]; + if (prefsID.length == 0 || bundle == nil) { + NSLog(@"%s: Can't determine app bundle ID", __func__); + return; + } + NSUserDefaults *prefs = [NSUserDefaults.alloc initWithSuiteName:prefsID]; + if (prefs == nil) { + NSLog(@"%s: Can't get app prefs for %@", __func__, prefsID); + return; + } + self.mainBundle = bundle; + self.mainPrefs = prefs; + } + + if (dockTile) { + if (self.updateObserver == nil) { + // Attach an observer that will update the high score or icon in the dock tile whenever it changes + self.updateObserver = [NSDistributedNotificationCenter.defaultCenter addObserverForName:DockUpdateNotificationKey object:nil queue:nil usingBlock:^(NSNotification *notification) { + [self updateDockTile:dockTile]; + }]; + } + [self updateDockTile:dockTile]; + } else if (self.updateObserver) { + // clean up observer + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self.updateObserver]; + self.updateObserver = nil; + } } -- (NSMenu *)dockMenu { - - // Create the menu - if (dockMenu == nil) - dockMenu = [[NSMenu alloc] init]; - else - [dockMenu removeAllItems]; - - // Let's Find the HighScore - CFPreferencesAppSynchronize(CFSTR("com.apple.examples.DockTile.ObjC.App")); - NSInteger highScore = CFPreferencesGetAppIntegerValue(CFSTR("HighScore"), CFSTR("com.apple.examples.DockTile.ObjC.App"), NULL); - - // Convert it into a string - NSString *highScoreAsString = [NSString stringWithFormat:@"%ld", (long)highScore]; - NSMenuItem *highScoreMenu = [[NSMenuItem alloc] initWithTitle:highScoreAsString action:NULL keyEquivalent:@""]; - - [dockMenu addItem: highScoreMenu]; - [highScoreMenu release]; - - return dockMenu; +- (NSMenu*) dockMenu { // gets ONLY called when app is not running and icon is in the Dock (see: `applicationDockMenu:`) + // Let the user choose an icon via the dock menu (#826) + return [SharedDockItem dockMenuForPrefs:self.mainPrefs bundle:self.mainBundle]; } -- (void)dealloc { - if (self.highScoreObserver) { - [[NSDistributedNotificationCenter defaultCenter] removeObserver:self.highScoreObserver]; - self.highScoreObserver = nil; - } - [dockMenu release]; - [super dealloc]; + +- (void) dealloc { + if (self.updateObserver) { + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self.updateObserver]; + self.updateObserver = nil; + } } @end diff --git a/ObjC/DockTilePlugIn/SharedDockItem.h b/ObjC/DockTilePlugIn/SharedDockItem.h new file mode 100644 index 0000000..3111ca2 --- /dev/null +++ b/ObjC/DockTilePlugIn/SharedDockItem.h @@ -0,0 +1,32 @@ +// +// SharedDockItem.h +// FAFv2 +// +// Created by Thomas Tempelmann on 05.08.21. +// Copyright © 2021 Thomas Tempelmann. All rights reserved. +// + +#import + +// The name of the default icon the app, as set in the Info.plist (`CFBundleIconFile`) +#define DEFAULT_ICON_NAME @"icon.icns" + +// The Info.plist key that lists the available icons the user can choose from +#define InfoKeyDockIconsToChooseFrom @"DockIconsToChooseFrom" + +// The userdefaults key specifying the dock's icon file +#define PrefsKeyDockIcon @"DockIcon" + +// The userdefaults key specifying the highscore that'll appear in the dock tile's badge +#define PrefsKeyHighScore @"HighScore" + +// The message we send to ask the DockTile plugin to refresh its icon and/or badge +static NSString *DockUpdateNotificationKey = @"com.apple.DockTile.RefreshDock"; + +@interface SharedDockItem : NSObject + +// A function that creates the dock's custom menu, +// used by both the main app and by the plugin when the app is not running ++ (NSMenu*) dockMenuForPrefs:(NSUserDefaults*)prefs bundle:(NSBundle*)bundle; + +@end diff --git a/ObjC/DockTilePlugIn/SharedDockItem.m b/ObjC/DockTilePlugIn/SharedDockItem.m new file mode 100644 index 0000000..1db8e81 --- /dev/null +++ b/ObjC/DockTilePlugIn/SharedDockItem.m @@ -0,0 +1,68 @@ +// +// SharedDockItem.m +// + +#import "SharedDockItem.h" + +static NSMenu *myDockMenu = nil; +static NSUserDefaults *mainPrefs = nil; + +@implementation SharedDockItem + ++ (NSMenu*) dockMenuForPrefs:(NSUserDefaults*)prefs bundle:(NSBundle*)bundle +{ + /* + * This method performs two things: + * 1. Add the current high score as a disabled menu item + * 2. Add a list of icon names, from which the user can then choose one. + * Note that the DockTile menu does not appear to support submenus - a submenu's contents will be flattened out to the top level. + */ + + [prefs synchronize]; + + // Create or empty the our menu + if (myDockMenu == nil) { + myDockMenu = [[NSMenu alloc] init]; + } else { + [myDockMenu removeAllItems]; + } + + // + // Add the HighScore to the menu + // + NSInteger highScore = [prefs integerForKey:PrefsKeyHighScore]; + NSString *highScoreAsString = [NSString stringWithFormat:@"High Score: %ld", (long)highScore]; + NSMenuItem *highScoreMenu = [[NSMenuItem alloc] initWithTitle:highScoreAsString action:NULL keyEquivalent:@""]; + [myDockMenu addItem: highScoreMenu]; + + // + // Add the icon choices to the menu + // + NSString *iconName = [prefs objectForKey:PrefsKeyDockIcon]; + NSArray *iconChoices = bundle.infoDictionary[InfoKeyDockIconsToChooseFrom]; + NSMenuItem *subMenu = [myDockMenu addItemWithTitle:@"Choose Dock Icon" action:nil keyEquivalent:@""]; + subMenu.enabled = YES; // while we use a submenu here, it won't show as a sub menu, unfortunately. + for (NSDictionary *d in iconChoices) { + NSString *key = d[@"title"]; + NSString *name = d[@"file name"]; + NSString *title = NSLocalizedString(key, @"DockIcon title"); // In case you'd want to localize the titles + NSMenuItem *menuItem = [subMenu.menu addItemWithTitle:title action:@selector(chooseIcon:) keyEquivalent:@""]; + menuItem.target = self; + menuItem.representedObject = name; + if ([name isEqualToString:iconName]) { + menuItem.state = NSControlStateValueOn; // checkmark the currently chosen icon + } + menuItem.enabled = YES; + } + mainPrefs = prefs; // needed in `chooseIcon:`, though we could have also put it into a dictionary assigned to `menuItem.representedObject` + + return myDockMenu; +} + ++ (void)chooseIcon:(NSMenuItem*)menuItem +{ + [mainPrefs setObject:menuItem.representedObject forKey:PrefsKeyDockIcon]; + [NSDistributedNotificationCenter.defaultCenter postNotificationName:DockUpdateNotificationKey object:nil]; +} + +@end diff --git a/ObjC/icon2.icns b/ObjC/icon2.icns new file mode 100755 index 0000000..7bb3b97 Binary files /dev/null and b/ObjC/icon2.icns differ diff --git a/ObjC/main.m b/ObjC/main.m index 29215fd..cc87e52 100644 --- a/ObjC/main.m +++ b/ObjC/main.m @@ -45,7 +45,7 @@ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF */ -#include +#import int main(int argc, char *argv[]) { diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 9f24a6d..eabea49 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # DockTile Sample + As Apple has removed any tutorials on how to write **NSDockTilePlugin**s so I Googled for one. This is an updated version of [Janetzko Helmut's](https://github.com/HelmutJ) Objective C [DockTile](https://github.com/HelmutJ/CocoaSampleCode/tree/master/DockTile) sample. -His version is 7 years out of date, so this version updates the code to work in Xcode 11. + +His version is many years out of date, so this version updates the code to work in Xcode 10 and later. ## Original Sample's ReadMe -DockTile application demonstrates the use of NSDockTile, and more importantly, the NSDockTilePlugIn protocol introduced in 10.6. The example includes two pieces: The DockTile app, and the plug-in, DockTilePlugIn. Building the target for the app will build and package both together. -You can think of DockTile as a terribly simple game. Your score goes up by 1 just by launching the app! So keep on launching the app over and over to reach new high scores. +DockTile application demonstrates the use of NSDockTile, and more importantly, the NSDockTilePlugIn protocol introduced in 10.6. + +The example includes two pieces: The DockTile app, and the plug-in, DockTilePlugIn. Building the target for the app will build and package both together. + +You can think of DockTile as a terribly simple game. Your score goes up by 1 just by launching the app. So keep on launching the app over and over to reach new high scores. The high score is shown in the dock tile, and the app window, where you can also reset it. The plug-in is useful as a way to show the score even when the application is not running. @@ -14,30 +19,43 @@ The whole application source code is implemented in the DockTileAppDelegate clas The dock tile plug-in shows off your high score even when the app is not running. The plug-in simply reads the high score from the defaults domain of the app, displays it as a badge on the dock tile, then updates it on receipt of a distributed notification that indicates when the high score changed. Some notes and things to watch out for: + * The project has two targets; the DockTileApp target also builds the DockTilePlugIn target, and includes it in the app package. * The plug-in goes inside the Contents/PlugIns folder in the app package. Just the file name goes in the app's Info.plist, as the value of NSDockTilePlugIn. * Remember to build the plug-in 32 and 64 bits. * The plug-in's principal class should be listed in the plug-in's Info.plist. * As of 10.6, SystemUIServer (the server which hosts the plug-ins) does not always notice when the plug-ins are updated. So during development, you may want to restart it after updating the plug-in. Most thorough steps are: Remove your app from the dock. Wait 3-5 seconds. killall Dock.  killall SystemUIServer. * If your app does not draw into the dock tile, the plug-in's updates will be in effect even when the app is running. So you can actually have your dock tile plug-in do all dock tile updating if you wish. In the DockTile example the app actually shows the score itself while it's running. - * Dock tile plug-ins should remember they are "guests" inside SystemUIServer, and take care not to destabilize or hog the process, or do anything that would block the main thread, such as networking, messaging, etc. The DockTile example itself uses two techniques to hear about the high score status: Registers for a distributed notification, and access the high score using CFPreferences out of the app's preferences domain. For instance the latter could have also been achieved using the NSUserDefaults addSuiteNamed: API, but that would have changed the global preferences domain list for the SystemUIServer—not a good idea. + * Dock tile plug-ins should remember they are "guests" inside SystemUIServer, and take care not to destabilize or hog the process, or do anything that would block the main thread, such as networking, messaging, etc. The DockTile example itself uses two techniques to hear about the high score status: Registers for a distributed notification, and access the high score using CFPreferences out of the app's preferences domain. For instance the latter could have also been achieved using the NSUserDefaults addSuiteNamed: API, but that would have changed the global preferences domain list for the SystemUIServer — not a good idea. + +## Development tips + +* The current version (1.2) was created in Xcode 10.1. It may have trouble opening in older Xcode versions. If it does, try taking the project file of version 1.0 and then add the new files (icon2.icns, SharedDockItem.m/.h) again, and also turn on ARC again, or re-add the missing `release` and `[super dealloc]` calls. + +* While macOS 11 (Big Sur) appears to properly reload the DockTilePlugin after it's been rebuilt (and as long as it's not set to "Keep in Dock"), older macOS versions (including 10.13) do not automatically reload the plugin. There, issue `killall Dock ; killall SystemUIServer` in Terminal after building a new version and before testing it. +## What the original sample showed -## What the original sample showed -Each time the app is opened a counter increments and the app's DockTile is updated with the new value. - -## Enhancement over original Sample -* Added DockMenu support - * When you right click the app's dock tile, while it is running, the popped up menu will display the current counter - * After setting the app to **Keep in Dock**, then shutting it down, when you right click the app's dock tile, now no longer running, the popped up menu will display the current counter - +Each time the app is opened a counter increments and the app's DockTile is updated with the new value. + +## Enhancement over original Sample + +* Added DockMenu support + * When you right click the app's dock tile, while it is running, the popped up menu will display the current counter + * After setting the app to **Keep in Dock**, then shutting it down, when you right click the app's dock tile, now no longer running, the popped up menu will display the current counter +* (v1.2) Added support for changing the Dock icon, with considering a different icon for macOS Big Sur (and later) by default. The icon can be changed via the dock tile's menu. This work was done by [Thomas Tempelmann](https://github.com/tempelmann/MacDockTileSample) + +## How to add multiple Dock Icons to choose from + +See the `Info.plist` (`DockTile.ObjC-Info.plist`) file for the main app. There's a `DockIconsToChooseFrom` key that lists pairs of titles and file names. The titles will appear in the dock menu, whereas the file names identify the icon files that are to be located inside the app's Resources folder. The titles are also localizable through the usual ".strings" files. ## Future -I hope to update this repo with example of how to do DockTile plugins in other languages. + +I hope to update this repo with example of how to do DockTile plugins in other languages. 1st should be a Xamarin/.NET sample. -If you would like to port this sample to another language and would like to add it to this repo, please fork the repo as Pull Requests are always welcome!! :D - -## License - -The DockTile project is under the [Microsoft Public License](https://opensource.org/licenses/MS-PL) except for a few portions of the code. See the [LICENSE.txt](LICENSE.txt) file for more details. Third-party libraries used by MonoGame are under their own licenses. Please refer to those libraries for details on the license they use. \ No newline at end of file +If you would like to port this sample to another language and would like to add it to this repo, please fork the repo as Pull Requests are always welcome!! :D + +## License + +The DockTile project is under the [Microsoft Public License](https://opensource.org/licenses/MS-PL) except for a few portions of the code. See the [LICENSE.txt](LICENSE.txt) file for more details.