2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 #import "AIDockController.h"
20 #import <Adium/AIInterfaceControllerProtocol.h>
21 #import <Adium/AIPreferenceControllerProtocol.h>
22 #import <AIUtilities/AIDictionaryAdditions.h>
23 #import <AIUtilities/AIFileManagerAdditions.h>
24 #import <AIUtilities/AIApplicationAdditions.h>
25 #import <Adium/AIIconState.h>
26 #import <Adium/IconFamily.h>
28 #define DOCK_DEFAULT_PREFS @"DockPrefs"
29 #define ICON_DISPLAY_DELAY 0.1
31 #define LAST_ICON_UPDATE_VERSION @"Adium:Last Icon Update Version"
33 #define CONTINUOUS_BOUNCE_INTERVAL 0
34 #define SINGLE_BOUNCE_INTERVAL 999
35 #define NO_BOUNCE_INTERVAL 1000
37 @interface AIDockController (PRIVATE)
38 - (void)_setNeedsDisplay;
40 - (void)animateIcon:(NSTimer *)timer;
41 - (void)_singleBounce;
42 - (BOOL)_continuousBounce;
43 - (void)_stopBouncing;
44 - (BOOL)_bounceWithInterval:(double)delay;
45 - (void)preferencesChanged:(NSNotification *)notification;
46 - (AIIconState *)iconStateFromStateDict:(NSDictionary *)stateDict folderPath:(NSString *)folderPath;
47 - (void)updateAppBundleIcon;
51 @interface NSWorkspace (NewTigerMethod)
52 - (BOOL)setIcon:(NSImage *)image forFile:(NSString *)fullPath options:(unsigned)options;
56 @implementation AIDockController
61 if ((self = [super init])) {
62 activeIconStateArray = [[NSMutableArray alloc] initWithObjects:@"Base",nil];
63 availableDynamicIconStateDict = [[NSMutableDictionary alloc] init];
64 currentIconState = nil;
65 currentAttentionRequest = -1;
66 currentBounceInterval = NO_BOUNCE_INTERVAL;
75 - (void)controllerDidLoad
77 NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
79 //Register our default preferences
80 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:DOCK_DEFAULT_PREFS
81 forClass:[self class]]
82 forGroup:PREF_GROUP_APPEARANCE];
84 //Observe pref changes
85 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
87 //We always want to stop bouncing when Adium is made active
88 [notificationCenter addObserver:self
89 selector:@selector(appWillChangeActive:)
90 name:NSApplicationWillBecomeActiveNotification
93 //We also stop bouncing when Adium is no longer active
94 [notificationCenter addObserver:self
95 selector:@selector(appWillChangeActive:)
96 name:NSApplicationWillResignActiveNotification
99 //If Adium has been upgraded since the last time we ran, re-apply the user's custom icon
100 NSString *lastVersion = [[NSUserDefaults standardUserDefaults] objectForKey:LAST_ICON_UPDATE_VERSION];
101 if (![[NSApp applicationVersion] isEqualToString:lastVersion]) {
102 [self updateAppBundleIcon];
103 [[NSUserDefaults standardUserDefaults] setObject:[NSApp applicationVersion] forKey:LAST_ICON_UPDATE_VERSION];
107 - (void)controllerWillClose
109 [[adium preferenceController] unregisterPreferenceObserver:self];
111 NSArray *stateArrayCopy;
112 NSEnumerator *enumerator;
115 //Reset our icon by removing all icon states (except for the base state)
116 stateArrayCopy = [activeIconStateArray copy]; //Work with a copy, since this array will change as we remove states
117 enumerator = [stateArrayCopy objectEnumerator];
118 [enumerator nextObject]; //Skip the first icon
119 while ((iconState = [enumerator nextObject])) {
120 [self removeIconStateNamed:iconState];
123 //Force the icon to update
126 [stateArrayCopy release];
131 * @brief Returns an array of available dock icon pack paths
133 - (NSArray *)availableDockIconPacks
135 NSEnumerator * folderPathEnumerator = [[adium allResourcesForName:FOLDER_DOCK_ICONS withExtensions:@"AdiumIcon"] objectEnumerator];
136 NSMutableArray * iconPackPaths = [NSMutableArray array]; //this will be the folder path for old packs, and the bundle resource path for new
138 NSBundle * xtraBundle;
139 while ((path = [folderPathEnumerator nextObject])) {
140 xtraBundle = [NSBundle bundleWithPath:path];
141 if (xtraBundle && ([[xtraBundle objectForInfoDictionaryKey:@"XtraBundleVersion"] intValue] == 1))//This checks for a new-style xtra
142 path = [xtraBundle resourcePath];
143 [iconPackPaths addObject:path];
145 return iconPackPaths;
150 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
151 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
153 if (!key || [key isEqualToString:KEY_ACTIVE_DOCK_ICON]) {
154 NSMutableDictionary *newAvailableIconStateDict;
157 //Load the new icon pack
158 iconPath = [adium pathOfPackWithName:[prefDict objectForKey:KEY_ACTIVE_DOCK_ICON]
159 extension:@"AdiumIcon"
160 resourceFolderName:FOLDER_DOCK_ICONS];
163 if ((newAvailableIconStateDict = [[self iconPackAtPath:iconPath] retain])) {
164 [availableIconStateDict release]; availableIconStateDict = newAvailableIconStateDict;
168 //Write the icon to the Adium application bundle so finder will see it
169 //On launch we only need to update the icon file if this is a new version of Adium. When preferences
170 //change we always want to update it
172 [self updateAppBundleIcon];
175 //Recomposite the icon
176 [self _setNeedsDisplay];
180 - (void)updateAppBundleIcon
184 image = [[[availableIconStateDict objectForKey:@"State"] objectForKey:@"Base"] image];
186 if ([NSApp isOnTigerOrBetter]) {
187 [[NSWorkspace sharedWorkspace] setIcon:image
188 forFile:[[NSBundle mainBundle] bundlePath]
192 NSString *icnsPath = [[NSBundle mainBundle] pathForResource:@"Adium" ofType:@"icns"];
193 IconFamily *iconFamily;
195 iconFamily = [IconFamily iconFamilyWithThumbnailsOfImage:image
196 usingImageInterpolation:NSImageInterpolationLow];
197 [iconFamily setAsCustomIconForFile:[[NSBundle mainBundle] bundlePath]];
198 [iconFamily writeToFile:icnsPath];
202 //Finder won't update Adium's icon to match the new one until it is restarted if we don't
203 //tell NSWorkspace to note the change.
204 [[NSWorkspace sharedWorkspace] noteFileSystemChanged:[[NSBundle mainBundle] bundlePath]];
208 //Icons ------------------------------------------------------------------------------------
209 - (void)_setNeedsDisplay
214 //Invoke a display after a short delay
215 [NSTimer scheduledTimerWithTimeInterval:ICON_DISPLAY_DELAY
217 selector:@selector(_buildIcon)
224 - (NSMutableDictionary *)iconPackAtPath:(NSString *)folderPath
226 NSMutableDictionary *iconStateDict;
227 NSDictionary *iconPackDict;
228 NSEnumerator *stateNameKeyEnumerator;
229 NSString *stateNameKey;
232 iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
234 //Process each state in the icon pack, adding it to the iconStateDict
235 iconStateDict = [NSMutableDictionary dictionary];
237 stateNameKeyEnumerator = [[[iconPackDict objectForKey:@"State"] allKeys] objectEnumerator];
238 while ((stateNameKey = [stateNameKeyEnumerator nextObject])) {
239 NSDictionary *stateDict;
240 AIIconState *iconState;
242 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:stateNameKey];
243 if ((iconState = [self iconStateFromStateDict:stateDict folderPath:folderPath])) {
244 [iconStateDict setObject:iconState forKey:stateNameKey];
248 return [NSMutableDictionary dictionaryWithObjectsAndKeys:[iconPackDict objectForKey:@"Description"], @"Description", iconStateDict, @"State", nil];
252 * @brief Get the name and preview steate for a dock icon pack
254 * @param outName Reference to an NSString, or NULL if this information is not needed
255 * @param outIconState Reference to an AIIconState, or NULL if this information is not needed
256 * @param folderPath The path to the dock icon pack
258 - (void)getName:(NSString **)outName previewState:(AIIconState **)outIconState forIconPackAtPath:(NSString *)folderPath
260 NSDictionary *iconPackDict;
261 NSDictionary *stateDict;
264 iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
266 //Load the preview state
267 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:@"Preview"];
269 if (outIconState) *outIconState = [self iconStateFromStateDict:stateDict folderPath:folderPath];
270 if (outName) *outName = [[iconPackDict objectForKey:@"Description"] objectForKey:@"Title"];
273 - (AIIconState *)previewStateForIconPackAtPath:(NSString *)folderPath
275 AIIconState *previewState = nil;
277 [self getName:NULL previewState:&previewState forIconPackAtPath:folderPath];
282 - (AIIconState *)iconStateFromStateDict:(NSDictionary *)stateDict folderPath:(NSString *)folderPath
284 AIIconState *iconState = nil;
286 if ([[stateDict objectForKey:@"Animated"] intValue]) { //Animated State
287 NSMutableDictionary *tempIconCache = [NSMutableDictionary dictionary];
288 NSArray *imageNameArray;
289 NSEnumerator *imageNameEnumerator;
291 NSMutableArray *imageArray;
292 BOOL overlay, looping;
295 //Get the state information
296 overlay = [[stateDict objectForKey:@"Overlay"] boolValue];
297 looping = [[stateDict objectForKey:@"Looping"] boolValue];
298 delay = [[stateDict objectForKey:@"Delay"] floatValue];
299 imageNameArray = [stateDict objectForKey:@"Images"];
300 imageNameEnumerator = [imageNameArray objectEnumerator];
303 imageArray = [NSMutableArray arrayWithCapacity:[imageNameArray count]];
304 while ((imageName = [imageNameEnumerator nextObject])) {
308 #define DOCK_ICON_INTERNAL_PATH @"../Shared Images/"
309 if ([imageName hasPrefix:DOCK_ICON_INTERNAL_PATH]) {
310 //Special hack for all the incorrectly made icon packs we have floating around out there :P
311 imageName = [imageName substringFromIndex:[DOCK_ICON_INTERNAL_PATH length]];
312 imagePath = [[NSBundle mainBundle] pathForResource:[[[imageName stringByDeletingPathExtension] stringByAppendingString:@"-localized"] stringByAppendingPathExtension:[imageName pathExtension]]
314 inDirectory:@"Shared Dock Icon Images"];
317 imagePath = [[NSBundle mainBundle] pathForResource:imageName
319 inDirectory:@"Shared Dock Icon Images"];
323 imagePath = [folderPath stringByAppendingPathComponent:imageName];
326 image = [tempIconCache objectForKey:imagePath]; //We re-use the same images for each state if possible to lower memory usage.
327 if (!image && imagePath) {
328 image = [[[NSImage alloc] initByReferencingFile:imagePath] autorelease];
329 if (image) [tempIconCache setObject:image forKey:imagePath];
332 if (image) [imageArray addObject:image];
336 if (delay != 0 && [imageArray count] != 0) {
337 iconState = [[AIIconState alloc] initWithImages:imageArray
342 NSLog(@"Invalid animated icon state (%@)",imageName);
345 } else { //Static State
351 imageName = [stateDict objectForKey:@"Image"];
353 if ([imageName hasPrefix:DOCK_ICON_INTERNAL_PATH]) {
354 //Special hack for all the incorrectly made icon packs we have floating around out there :P
355 imageName = [imageName substringFromIndex:[DOCK_ICON_INTERNAL_PATH length]];
356 imagePath = [[NSBundle mainBundle] pathForResource:[[[imageName stringByDeletingPathExtension] stringByAppendingString:@"-localized"] stringByAppendingPathExtension:[imageName pathExtension]]
358 inDirectory:@"Shared Dock Icon Images"];
360 imagePath = [[NSBundle mainBundle] pathForResource:imageName
362 inDirectory:@"Shared Dock Icon Images"];
365 imagePath = [folderPath stringByAppendingPathComponent:imageName];
368 //Get the state information
369 image = [[NSImage alloc] initByReferencingFile:imagePath];
370 overlay = [[stateDict objectForKey:@"Overlay"] boolValue];
373 iconState = [[AIIconState alloc] initWithImage:image overlay:overlay];
377 return [iconState autorelease];
380 //Set an icon state from our currently loaded icon pack
381 - (void)setIconStateNamed:(NSString *)inName
383 if (![activeIconStateArray containsObject:inName]) {
384 [activeIconStateArray addObject:inName]; //Add the name to our array
385 [self _setNeedsDisplay]; //Redisplay our icon
389 //Remove an active icon state
390 - (void)removeIconStateNamed:(NSString *)inName
392 if ([activeIconStateArray containsObject:inName]) {
393 [activeIconStateArray removeObject:inName]; //Remove the name from our array
395 [self _setNeedsDisplay]; //Redisplay our icon
400 * @brief Does the current icon know how to display a given state?
402 - (BOOL)currentIconSupportsIconStateNamed:(NSString *)inName
404 return ([[availableIconStateDict objectForKey:@"State"] objectForKey:inName] != nil);
407 //Set a custom icon state
408 - (void)setIconState:(AIIconState *)iconState named:(NSString *)inName
410 [availableDynamicIconStateDict setObject:iconState forKey:inName]; //Add the new state to our available dict
411 [self setIconStateNamed:inName]; //Set it
414 //Build/Pre-render the icon images, start/stop animation
417 NSMutableArray *iconStates = [NSMutableArray array];
418 NSDictionary *availableIcons;
419 NSEnumerator *enumerator;
423 //Stop any existing animation
424 [animationTimer invalidate]; [animationTimer release]; animationTimer = nil;
425 if (observingFlash) {
426 [[adium interfaceController] unregisterFlashObserver:self];
430 //Build an array of the valid active icon states
431 availableIcons = [availableIconStateDict objectForKey:@"State"];
432 enumerator = [activeIconStateArray objectEnumerator];
433 while ((name = [enumerator nextObject])) {
434 if ((state = [availableIcons objectForKey:name]) || (state = [availableDynamicIconStateDict objectForKey:name])) {
435 [iconStates addObject:state];
439 //Generate the composited icon state
440 [currentIconState release];
441 currentIconState = [[AIIconState alloc] initByCompositingStates:iconStates];
444 if (![currentIconState animated]) { //Static icon
445 NSImage *image = [currentIconState image];
447 [[NSApplication sharedApplication] setApplicationIconImage:image];
450 } else { //Animated icon
451 //Our dock icon can run its animation at any speed, but we want to try and sync it with the global Adium flashing. To do this, we delay starting our timer until the next flash occurs.
452 [[adium interfaceController] registerFlashObserver:self];
453 observingFlash = YES;
455 //Set the first frame of our animation
456 [self animateIcon:nil]; //Set the icon and move to the next frame
462 - (void)flash:(int)value
464 //Start the flash timer
465 animationTimer = [[NSTimer scheduledTimerWithTimeInterval:[currentIconState animationDelay]
467 selector:@selector(animateIcon:)
469 repeats:YES] retain];
472 [self animateIcon:animationTimer]; //Set the icon and move to the next frame
474 //Once our animations stops, we no longer need to observe flashing
475 [[adium interfaceController] unregisterFlashObserver:self];
479 //Move the dock to the next animation frame (Assumes the current state is animated)
480 - (void)animateIcon:(NSTimer *)timer
484 //Move to the next image
486 [currentIconState nextFrame];
490 image = [currentIconState image];
492 [[NSApplication sharedApplication] setApplicationIconImage:image];
497 //returns the % of the dock icon's full size that it currently is (0.0 - 1.0)
498 - (float)dockIconScale
500 NSSize trueSize = [[NSScreen mainScreen] visibleFrame].size;
501 NSSize availableSize = [[NSScreen mainScreen] frame].size;
503 int dHeight = availableSize.height - trueSize.height;
504 int dWidth = availableSize.width - trueSize.width;
507 if (dHeight != 22) { //dock is on the bottom
508 if (dHeight == 26) { //dock is hidden
509 } else { //dock is not hidden
510 dockScale = (dHeight-22)/128.0;
512 } else if (dWidth != 0) { //dock is on the side
513 if (dWidth == 4) { //dock is hidden
514 } else { //dock is not hidden
515 dockScale = (dWidth)/128.0;
519 //Add support for multiple monitors
522 if (dockScale <= 0 || dockScale > 1.0) {
530 * @brief Return the dock icon image without any auxiliary states
532 - (NSImage *)baseApplicationIconImage
534 NSDictionary *availableIcons = [availableIconStateDict objectForKey:@"State"];
535 AIIconState *baseState;
536 NSImage *baseApplicationIconImage;
538 if ((baseState = [availableIcons objectForKey:@"Base"])) {
539 AIIconState *iconState = [[[AIIconState alloc] initByCompositingStates:[NSArray arrayWithObject:baseState]] autorelease];
540 baseApplicationIconImage = [iconState image];
542 baseApplicationIconImage = nil;
545 return baseApplicationIconImage;
548 //Bouncing -------------------------------------------------------------------------------------------------------------
549 #pragma mark Bouncing
552 * @brief Perform a bouncing behavior
554 * @result YES if the behavior is ongoing; NO if it isn't (because it is immediately complete or some other, faster continuous behavior is in progress)
556 - (BOOL)performBehavior:(AIDockBehavior)behavior
558 BOOL ongoingBehavior = NO;
560 //Start up the new behavior
562 case AIDockBehaviorStopBouncing: {
563 [self _stopBouncing];
566 case AIDockBehaviorBounceOnce: {
567 if (currentBounceInterval >= SINGLE_BOUNCE_INTERVAL) {
568 currentBounceInterval = SINGLE_BOUNCE_INTERVAL;
569 [self _singleBounce];
573 case AIDockBehaviorBounceRepeatedly: ongoingBehavior = [self _continuousBounce]; break;
574 case AIDockBehaviorBounceDelay_FiveSeconds: ongoingBehavior = [self _bounceWithInterval:5.0]; break;
575 case AIDockBehaviorBounceDelay_TenSeconds: ongoingBehavior = [self _bounceWithInterval:10.0]; break;
576 case AIDockBehaviorBounceDelay_FifteenSeconds: ongoingBehavior = [self _bounceWithInterval:15.0]; break;
577 case AIDockBehaviorBounceDelay_ThirtySeconds: ongoingBehavior = [self _bounceWithInterval:30.0]; break;
578 case AIDockBehaviorBounceDelay_OneMinute: ongoingBehavior = [self _bounceWithInterval:60.0]; break;
582 return ongoingBehavior;
585 //Return a string description of the bouncing behavior
586 - (NSString *)descriptionForBehavior:(AIDockBehavior)behavior
591 case AIDockBehaviorStopBouncing: desc = AILocalizedString(@"None",nil); break;
592 case AIDockBehaviorBounceOnce: desc = AILocalizedString(@"Once",nil); break;
593 case AIDockBehaviorBounceRepeatedly: desc = AILocalizedString(@"Repeatedly",nil); break;
594 case AIDockBehaviorBounceDelay_FiveSeconds: desc = AILocalizedString(@"Every 5 Seconds",nil); break;
595 case AIDockBehaviorBounceDelay_TenSeconds: desc = AILocalizedString(@"Every 10 Seconds",nil); break;
596 case AIDockBehaviorBounceDelay_FifteenSeconds: desc = AILocalizedString(@"Every 15 Seconds",nil); break;
597 case AIDockBehaviorBounceDelay_ThirtySeconds: desc = AILocalizedString(@"Every 30 Seconds",nil); break;
598 case AIDockBehaviorBounceDelay_OneMinute: desc = AILocalizedString(@"Every 60 Seconds",nil); break;
599 default: desc=@""; break;
606 * @brief Start a delayed, repeated bounce
608 * @result YES if we are now bouncing more frequently than before; NO if this call had no effect
610 - (BOOL)_bounceWithInterval:(NSTimeInterval)delay
612 BOOL ongoingBehavior;
614 //Bounce only if the new delay is a faster bounce than the current one
615 if (delay < currentBounceInterval) {
616 [self _singleBounce]; // do one right away
618 currentBounceInterval = delay;
620 bounceTimer = [[NSTimer scheduledTimerWithTimeInterval:delay
622 selector:@selector(bounceWithTimer:)
624 repeats:YES] retain];
626 ongoingBehavior = YES;
628 ongoingBehavior = NO;
631 return ongoingBehavior;
634 //Activated by the time after each delay
635 - (void)bounceWithTimer:(NSTimer *)timer
638 [self _singleBounce];
641 //Bounce once via NSApp's NSInformationalRequest (also used by the timer to perform a single bounce)
642 - (void)_singleBounce
644 if ([NSApp respondsToSelector:@selector(requestUserAttention:)]) {
645 currentAttentionRequest = [NSApp requestUserAttention:NSInformationalRequest];
650 * @brief Bounce continuously via NSApp's NSCriticalRequest
652 * We will bounce until we become the active application or our dock icon is clicked
654 * @result YES if we are now bouncing more frequently than before; NO if this call had no effect
656 - (BOOL)_continuousBounce
658 BOOL ongoingBehavior;
660 if (CONTINUOUS_BOUNCE_INTERVAL < currentBounceInterval) {
661 currentBounceInterval = CONTINUOUS_BOUNCE_INTERVAL;
662 if ([NSApp respondsToSelector:@selector(requestUserAttention:)]) {
663 currentAttentionRequest = [NSApp requestUserAttention:NSCriticalRequest];
666 ongoingBehavior = YES;
668 ongoingBehavior = NO;
671 return ongoingBehavior;
675 - (void)_stopBouncing
679 [bounceTimer invalidate]; [bounceTimer release]; bounceTimer = nil;
682 //Stop any continuous bouncing
683 if (currentAttentionRequest != -1) {
684 if ([NSApp respondsToSelector:@selector(cancelUserAttentionRequest:)]) {
685 [NSApp cancelUserAttentionRequest:currentAttentionRequest];
687 currentAttentionRequest = -1;
690 currentBounceInterval = NO_BOUNCE_INTERVAL;
694 - (void)appWillChangeActive:(NSNotification *)notification
696 [self _stopBouncing]; //Stop any bouncing