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;
50 @implementation AIDockController
55 if ((self = [super init])) {
56 activeIconStateArray = [[NSMutableArray alloc] initWithObjects:@"Base",nil];
57 availableDynamicIconStateDict = [[NSMutableDictionary alloc] init];
58 currentIconState = nil;
59 currentAttentionRequest = -1;
60 currentBounceInterval = NO_BOUNCE_INTERVAL;
69 - (void)controllerDidLoad
71 NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
73 //Register our default preferences
74 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:DOCK_DEFAULT_PREFS
75 forClass:[self class]]
76 forGroup:PREF_GROUP_APPEARANCE];
78 //Observe pref changes
79 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
81 //We always want to stop bouncing when Adium is made active
82 [notificationCenter addObserver:self
83 selector:@selector(appWillChangeActive:)
84 name:NSApplicationWillBecomeActiveNotification
87 //We also stop bouncing when Adium is no longer active
88 [notificationCenter addObserver:self
89 selector:@selector(appWillChangeActive:)
90 name:NSApplicationWillResignActiveNotification
93 //If Adium has been upgraded since the last time we ran, re-apply the user's custom icon
94 NSString *lastVersion = [[NSUserDefaults standardUserDefaults] objectForKey:LAST_ICON_UPDATE_VERSION];
95 if (![[NSApp applicationVersion] isEqualToString:lastVersion]) {
96 [self updateAppBundleIcon];
97 [[NSUserDefaults standardUserDefaults] setObject:[NSApp applicationVersion] forKey:LAST_ICON_UPDATE_VERSION];
101 - (void)controllerWillClose
103 [[adium preferenceController] unregisterPreferenceObserver:self];
105 NSArray *stateArrayCopy;
106 NSEnumerator *enumerator;
109 //Reset our icon by removing all icon states (except for the base state)
110 stateArrayCopy = [activeIconStateArray copy]; //Work with a copy, since this array will change as we remove states
111 enumerator = [stateArrayCopy objectEnumerator];
112 [enumerator nextObject]; //Skip the first icon
113 while ((iconState = [enumerator nextObject])) {
114 [self removeIconStateNamed:iconState];
117 //Force the icon to update
120 [stateArrayCopy release];
125 * @brief Returns an array of available dock icon pack paths
127 - (NSArray *)availableDockIconPacks
129 NSEnumerator * folderPathEnumerator = [[adium allResourcesForName:FOLDER_DOCK_ICONS withExtensions:@"AdiumIcon"] objectEnumerator];
130 NSMutableArray * iconPackPaths = [NSMutableArray array]; //this will be the folder path for old packs, and the bundle resource path for new
132 NSBundle * xtraBundle;
133 while ((path = [folderPathEnumerator nextObject])) {
134 xtraBundle = [NSBundle bundleWithPath:path];
135 if (xtraBundle && ([[xtraBundle objectForInfoDictionaryKey:@"XtraBundleVersion"] intValue] == 1))//This checks for a new-style xtra
136 path = [xtraBundle resourcePath];
137 [iconPackPaths addObject:path];
139 return iconPackPaths;
144 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
145 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
147 if (!key || [key isEqualToString:KEY_ACTIVE_DOCK_ICON]) {
148 NSMutableDictionary *newAvailableIconStateDict;
151 //Load the new icon pack
152 iconPath = [adium pathOfPackWithName:[prefDict objectForKey:KEY_ACTIVE_DOCK_ICON]
153 extension:@"AdiumIcon"
154 resourceFolderName:FOLDER_DOCK_ICONS];
157 if ((newAvailableIconStateDict = [[self iconPackAtPath:iconPath] retain])) {
158 [availableIconStateDict release]; availableIconStateDict = newAvailableIconStateDict;
162 //Write the icon to the Adium application bundle so finder will see it
163 //On launch we only need to update the icon file if this is a new version of Adium. When preferences
164 //change we always want to update it
166 [self updateAppBundleIcon];
169 //Recomposite the icon
170 [self _setNeedsDisplay];
174 - (void)updateAppBundleIcon
178 image = [[[availableIconStateDict objectForKey:@"State"] objectForKey:@"ApplicationIcon"] image];
179 if (!image) image = [[[availableIconStateDict objectForKey:@"State"] objectForKey:@"Base"] image];
182 [[NSWorkspace sharedWorkspace] setIcon:image
183 forFile:[[NSBundle mainBundle] bundlePath]
186 //Finder won't update Adium's icon to match the new one until it is restarted if we don't
187 //tell NSWorkspace to note the change.
188 [[NSWorkspace sharedWorkspace] noteFileSystemChanged:[[NSBundle mainBundle] bundlePath]];
192 //Icons ------------------------------------------------------------------------------------
193 - (void)_setNeedsDisplay
198 //Invoke a display after a short delay
199 [NSTimer scheduledTimerWithTimeInterval:ICON_DISPLAY_DELAY
201 selector:@selector(_buildIcon)
208 - (NSMutableDictionary *)iconPackAtPath:(NSString *)folderPath
210 NSMutableDictionary *iconStateDict;
211 NSDictionary *iconPackDict;
212 NSEnumerator *stateNameKeyEnumerator;
213 NSString *stateNameKey;
216 iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
218 //Process each state in the icon pack, adding it to the iconStateDict
219 iconStateDict = [NSMutableDictionary dictionary];
221 stateNameKeyEnumerator = [[[iconPackDict objectForKey:@"State"] allKeys] objectEnumerator];
222 while ((stateNameKey = [stateNameKeyEnumerator nextObject])) {
223 NSDictionary *stateDict;
224 AIIconState *iconState;
226 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:stateNameKey];
227 if ((iconState = [self iconStateFromStateDict:stateDict folderPath:folderPath])) {
228 [iconStateDict setObject:iconState forKey:stateNameKey];
232 return [NSMutableDictionary dictionaryWithObjectsAndKeys:[iconPackDict objectForKey:@"Description"], @"Description", iconStateDict, @"State", nil];
236 * @brief Get the name and preview steate for a dock icon pack
238 * @param outName Reference to an NSString, or NULL if this information is not needed
239 * @param outIconState Reference to an AIIconState, or NULL if this information is not needed
240 * @param folderPath The path to the dock icon pack
242 - (void)getName:(NSString **)outName previewState:(AIIconState **)outIconState forIconPackAtPath:(NSString *)folderPath
244 NSDictionary *iconPackDict;
245 NSDictionary *stateDict;
248 iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
250 //Load the preview state
251 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:@"Preview"];
253 if (outIconState) *outIconState = [self iconStateFromStateDict:stateDict folderPath:folderPath];
254 if (outName) *outName = [[iconPackDict objectForKey:@"Description"] objectForKey:@"Title"];
257 - (AIIconState *)previewStateForIconPackAtPath:(NSString *)folderPath
259 AIIconState *previewState = nil;
261 [self getName:NULL previewState:&previewState forIconPackAtPath:folderPath];
266 - (AIIconState *)iconStateFromStateDict:(NSDictionary *)stateDict folderPath:(NSString *)folderPath
268 AIIconState *iconState = nil;
270 if ([[stateDict objectForKey:@"Animated"] intValue]) { //Animated State
271 NSMutableDictionary *tempIconCache = [NSMutableDictionary dictionary];
272 NSArray *imageNameArray;
273 NSEnumerator *imageNameEnumerator;
275 NSMutableArray *imageArray;
276 BOOL overlay, looping;
279 //Get the state information
280 overlay = [[stateDict objectForKey:@"Overlay"] boolValue];
281 looping = [[stateDict objectForKey:@"Looping"] boolValue];
282 delay = [[stateDict objectForKey:@"Delay"] floatValue];
283 imageNameArray = [stateDict objectForKey:@"Images"];
284 imageNameEnumerator = [imageNameArray objectEnumerator];
287 imageArray = [NSMutableArray arrayWithCapacity:[imageNameArray count]];
288 while ((imageName = [imageNameEnumerator nextObject])) {
292 #define DOCK_ICON_INTERNAL_PATH @"../Shared Images/"
293 if ([imageName hasPrefix:DOCK_ICON_INTERNAL_PATH]) {
294 //Special hack for all the incorrectly made icon packs we have floating around out there :P
295 imageName = [imageName substringFromIndex:[DOCK_ICON_INTERNAL_PATH length]];
296 imagePath = [[NSBundle mainBundle] pathForResource:[[[imageName stringByDeletingPathExtension] stringByAppendingString:@"-localized"] stringByAppendingPathExtension:[imageName pathExtension]]
298 inDirectory:@"Shared Dock Icon Images"];
301 imagePath = [[NSBundle mainBundle] pathForResource:imageName
303 inDirectory:@"Shared Dock Icon Images"];
307 imagePath = [folderPath stringByAppendingPathComponent:imageName];
310 image = [tempIconCache objectForKey:imagePath]; //We re-use the same images for each state if possible to lower memory usage.
311 if (!image && imagePath) {
312 image = [[[NSImage alloc] initByReferencingFile:imagePath] autorelease];
313 if (image) [tempIconCache setObject:image forKey:imagePath];
316 if (image) [imageArray addObject:image];
320 if (delay != 0 && [imageArray count] != 0) {
321 iconState = [[AIIconState alloc] initWithImages:imageArray
326 NSLog(@"Invalid animated icon state (%@)",imageName);
329 } else { //Static State
335 imageName = [stateDict objectForKey:@"Image"];
337 if ([imageName hasPrefix:DOCK_ICON_INTERNAL_PATH]) {
338 //Special hack for all the incorrectly made icon packs we have floating around out there :P
339 imageName = [imageName substringFromIndex:[DOCK_ICON_INTERNAL_PATH length]];
340 imagePath = [[NSBundle mainBundle] pathForResource:[[[imageName stringByDeletingPathExtension] stringByAppendingString:@"-localized"] stringByAppendingPathExtension:[imageName pathExtension]]
342 inDirectory:@"Shared Dock Icon Images"];
344 imagePath = [[NSBundle mainBundle] pathForResource:imageName
346 inDirectory:@"Shared Dock Icon Images"];
349 imagePath = [folderPath stringByAppendingPathComponent:imageName];
352 //Get the state information
353 image = [[NSImage alloc] initByReferencingFile:imagePath];
354 overlay = [[stateDict objectForKey:@"Overlay"] boolValue];
357 iconState = [[AIIconState alloc] initWithImage:image overlay:overlay];
361 return [iconState autorelease];
364 //Set an icon state from our currently loaded icon pack
365 - (void)setIconStateNamed:(NSString *)inName
367 if (![activeIconStateArray containsObject:inName]) {
368 [activeIconStateArray addObject:inName]; //Add the name to our array
369 [self _setNeedsDisplay]; //Redisplay our icon
373 //Remove an active icon state
374 - (void)removeIconStateNamed:(NSString *)inName
376 if ([activeIconStateArray containsObject:inName]) {
377 [activeIconStateArray removeObject:inName]; //Remove the name from our array
379 [self _setNeedsDisplay]; //Redisplay our icon
384 * @brief Does the current icon know how to display a given state?
386 - (BOOL)currentIconSupportsIconStateNamed:(NSString *)inName
388 return ([[availableIconStateDict objectForKey:@"State"] objectForKey:inName] != nil);
391 //Set a custom icon state
392 - (void)setIconState:(AIIconState *)iconState named:(NSString *)inName
394 [availableDynamicIconStateDict setObject:iconState forKey:inName]; //Add the new state to our available dict
395 [self setIconStateNamed:inName]; //Set it
398 //Build/Pre-render the icon images, start/stop animation
401 NSMutableArray *iconStates = [NSMutableArray array];
402 NSDictionary *availableIcons;
403 NSEnumerator *enumerator;
407 //Stop any existing animation
408 [animationTimer invalidate]; [animationTimer release]; animationTimer = nil;
409 if (observingFlash) {
410 [[adium interfaceController] unregisterFlashObserver:self];
414 //Build an array of the valid active icon states
415 availableIcons = [availableIconStateDict objectForKey:@"State"];
416 enumerator = [activeIconStateArray objectEnumerator];
417 while ((name = [enumerator nextObject])) {
418 if ((state = [availableIcons objectForKey:name]) || (state = [availableDynamicIconStateDict objectForKey:name])) {
419 [iconStates addObject:state];
423 //Generate the composited icon state
424 [currentIconState release];
425 currentIconState = [[AIIconState alloc] initByCompositingStates:iconStates];
428 if (![currentIconState animated]) { //Static icon
429 NSImage *image = [currentIconState image];
431 [[NSApplication sharedApplication] setApplicationIconImage:image];
434 } else { //Animated icon
435 //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.
436 [[adium interfaceController] registerFlashObserver:self];
437 observingFlash = YES;
439 //Set the first frame of our animation
440 [self animateIcon:nil]; //Set the icon and move to the next frame
446 - (void)flash:(int)value
448 //Start the flash timer
449 animationTimer = [[NSTimer scheduledTimerWithTimeInterval:[currentIconState animationDelay]
451 selector:@selector(animateIcon:)
453 repeats:YES] retain];
456 [self animateIcon:animationTimer]; //Set the icon and move to the next frame
458 //Once our animations stops, we no longer need to observe flashing
459 [[adium interfaceController] unregisterFlashObserver:self];
463 //Move the dock to the next animation frame (Assumes the current state is animated)
464 - (void)animateIcon:(NSTimer *)timer
468 //Move to the next image
470 [currentIconState nextFrame];
474 image = [currentIconState image];
476 [[NSApplication sharedApplication] setApplicationIconImage:image];
481 //returns the % of the dock icon's full size that it currently is (0.0 - 1.0)
482 - (float)dockIconScale
484 NSSize trueSize = [[NSScreen mainScreen] visibleFrame].size;
485 NSSize availableSize = [[NSScreen mainScreen] frame].size;
487 int dHeight = availableSize.height - trueSize.height;
488 int dWidth = availableSize.width - trueSize.width;
491 if (dHeight != 22) { //dock is on the bottom
492 if (dHeight == 26) { //dock is hidden
493 } else { //dock is not hidden
494 dockScale = (dHeight-22)/128.0;
496 } else if (dWidth != 0) { //dock is on the side
497 if (dWidth == 4) { //dock is hidden
498 } else { //dock is not hidden
499 dockScale = (dWidth)/128.0;
503 //Add support for multiple monitors
506 if (dockScale <= 0 || dockScale > 1.0) {
514 * @brief Return the dock icon image without any auxiliary states
516 - (NSImage *)baseApplicationIconImage
518 NSDictionary *availableIcons = [availableIconStateDict objectForKey:@"State"];
519 AIIconState *baseState;
520 NSImage *baseApplicationIconImage;
522 if ((baseState = [availableIcons objectForKey:@"Base"])) {
523 AIIconState *iconState = [[[AIIconState alloc] initByCompositingStates:[NSArray arrayWithObject:baseState]] autorelease];
524 baseApplicationIconImage = [iconState image];
526 baseApplicationIconImage = nil;
529 return baseApplicationIconImage;
532 //Bouncing -------------------------------------------------------------------------------------------------------------
533 #pragma mark Bouncing
536 * @brief Perform a bouncing behavior
538 * @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)
540 - (BOOL)performBehavior:(AIDockBehavior)behavior
542 BOOL ongoingBehavior = NO;
544 //Start up the new behavior
546 case AIDockBehaviorStopBouncing: {
547 [self _stopBouncing];
550 case AIDockBehaviorBounceOnce: {
551 if (currentBounceInterval >= SINGLE_BOUNCE_INTERVAL) {
552 currentBounceInterval = SINGLE_BOUNCE_INTERVAL;
553 [self _singleBounce];
557 case AIDockBehaviorBounceRepeatedly: ongoingBehavior = [self _continuousBounce]; break;
558 case AIDockBehaviorBounceDelay_FiveSeconds: ongoingBehavior = [self _bounceWithInterval:5.0]; break;
559 case AIDockBehaviorBounceDelay_TenSeconds: ongoingBehavior = [self _bounceWithInterval:10.0]; break;
560 case AIDockBehaviorBounceDelay_FifteenSeconds: ongoingBehavior = [self _bounceWithInterval:15.0]; break;
561 case AIDockBehaviorBounceDelay_ThirtySeconds: ongoingBehavior = [self _bounceWithInterval:30.0]; break;
562 case AIDockBehaviorBounceDelay_OneMinute: ongoingBehavior = [self _bounceWithInterval:60.0]; break;
566 return ongoingBehavior;
569 //Return a string description of the bouncing behavior
570 - (NSString *)descriptionForBehavior:(AIDockBehavior)behavior
575 case AIDockBehaviorStopBouncing: desc = AILocalizedString(@"None",nil); break;
576 case AIDockBehaviorBounceOnce: desc = AILocalizedString(@"Once",nil); break;
577 case AIDockBehaviorBounceRepeatedly: desc = AILocalizedString(@"Repeatedly",nil); break;
578 case AIDockBehaviorBounceDelay_FiveSeconds: desc = AILocalizedString(@"Every 5 Seconds",nil); break;
579 case AIDockBehaviorBounceDelay_TenSeconds: desc = AILocalizedString(@"Every 10 Seconds",nil); break;
580 case AIDockBehaviorBounceDelay_FifteenSeconds: desc = AILocalizedString(@"Every 15 Seconds",nil); break;
581 case AIDockBehaviorBounceDelay_ThirtySeconds: desc = AILocalizedString(@"Every 30 Seconds",nil); break;
582 case AIDockBehaviorBounceDelay_OneMinute: desc = AILocalizedString(@"Every 60 Seconds",nil); break;
583 default: desc=@""; break;
590 * @brief Start a delayed, repeated bounce
592 * @result YES if we are now bouncing more frequently than before; NO if this call had no effect
594 - (BOOL)_bounceWithInterval:(NSTimeInterval)delay
596 BOOL ongoingBehavior;
598 //Bounce only if the new delay is a faster bounce than the current one
599 if (delay < currentBounceInterval) {
600 [self _singleBounce]; // do one right away
602 currentBounceInterval = delay;
604 bounceTimer = [[NSTimer scheduledTimerWithTimeInterval:delay
606 selector:@selector(bounceWithTimer:)
608 repeats:YES] retain];
610 ongoingBehavior = YES;
612 ongoingBehavior = NO;
615 return ongoingBehavior;
618 //Activated by the time after each delay
619 - (void)bounceWithTimer:(NSTimer *)timer
622 [self _singleBounce];
625 //Bounce once via NSApp's NSInformationalRequest (also used by the timer to perform a single bounce)
626 - (void)_singleBounce
628 if ([NSApp respondsToSelector:@selector(requestUserAttention:)]) {
629 currentAttentionRequest = [NSApp requestUserAttention:NSInformationalRequest];
634 * @brief Bounce continuously via NSApp's NSCriticalRequest
636 * We will bounce until we become the active application or our dock icon is clicked
638 * @result YES if we are now bouncing more frequently than before; NO if this call had no effect
640 - (BOOL)_continuousBounce
642 BOOL ongoingBehavior;
644 if (CONTINUOUS_BOUNCE_INTERVAL < currentBounceInterval) {
645 currentBounceInterval = CONTINUOUS_BOUNCE_INTERVAL;
646 if ([NSApp respondsToSelector:@selector(requestUserAttention:)]) {
647 currentAttentionRequest = [NSApp requestUserAttention:NSCriticalRequest];
650 ongoingBehavior = YES;
652 ongoingBehavior = NO;
655 return ongoingBehavior;
659 - (void)_stopBouncing
663 [bounceTimer invalidate]; [bounceTimer release]; bounceTimer = nil;
666 //Stop any continuous bouncing
667 if (currentAttentionRequest != -1) {
668 if ([NSApp respondsToSelector:@selector(cancelUserAttentionRequest:)]) {
669 [NSApp cancelUserAttentionRequest:currentAttentionRequest];
671 currentAttentionRequest = -1;
674 currentBounceInterval = NO_BOUNCE_INTERVAL;
678 - (void)appWillChangeActive:(NSNotification *)notification
680 [self _stopBouncing]; //Stop any bouncing