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:@"Base"] image];
180 [[NSWorkspace sharedWorkspace] setIcon:image
181 forFile:[[NSBundle mainBundle] bundlePath]
184 //Finder won't update Adium's icon to match the new one until it is restarted if we don't
185 //tell NSWorkspace to note the change.
186 [[NSWorkspace sharedWorkspace] noteFileSystemChanged:[[NSBundle mainBundle] bundlePath]];
190 //Icons ------------------------------------------------------------------------------------
191 - (void)_setNeedsDisplay
196 //Invoke a display after a short delay
197 [NSTimer scheduledTimerWithTimeInterval:ICON_DISPLAY_DELAY
199 selector:@selector(_buildIcon)
206 - (NSMutableDictionary *)iconPackAtPath:(NSString *)folderPath
208 NSMutableDictionary *iconStateDict;
209 NSDictionary *iconPackDict;
210 NSEnumerator *stateNameKeyEnumerator;
211 NSString *stateNameKey;
214 iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
216 //Process each state in the icon pack, adding it to the iconStateDict
217 iconStateDict = [NSMutableDictionary dictionary];
219 stateNameKeyEnumerator = [[[iconPackDict objectForKey:@"State"] allKeys] objectEnumerator];
220 while ((stateNameKey = [stateNameKeyEnumerator nextObject])) {
221 NSDictionary *stateDict;
222 AIIconState *iconState;
224 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:stateNameKey];
225 if ((iconState = [self iconStateFromStateDict:stateDict folderPath:folderPath])) {
226 [iconStateDict setObject:iconState forKey:stateNameKey];
230 return [NSMutableDictionary dictionaryWithObjectsAndKeys:[iconPackDict objectForKey:@"Description"], @"Description", iconStateDict, @"State", nil];
234 * @brief Get the name and preview steate for a dock icon pack
236 * @param outName Reference to an NSString, or NULL if this information is not needed
237 * @param outIconState Reference to an AIIconState, or NULL if this information is not needed
238 * @param folderPath The path to the dock icon pack
240 - (void)getName:(NSString **)outName previewState:(AIIconState **)outIconState forIconPackAtPath:(NSString *)folderPath
242 NSDictionary *iconPackDict;
243 NSDictionary *stateDict;
246 iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
248 //Load the preview state
249 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:@"Preview"];
251 if (outIconState) *outIconState = [self iconStateFromStateDict:stateDict folderPath:folderPath];
252 if (outName) *outName = [[iconPackDict objectForKey:@"Description"] objectForKey:@"Title"];
255 - (AIIconState *)previewStateForIconPackAtPath:(NSString *)folderPath
257 AIIconState *previewState = nil;
259 [self getName:NULL previewState:&previewState forIconPackAtPath:folderPath];
264 - (AIIconState *)iconStateFromStateDict:(NSDictionary *)stateDict folderPath:(NSString *)folderPath
266 AIIconState *iconState = nil;
268 if ([[stateDict objectForKey:@"Animated"] intValue]) { //Animated State
269 NSMutableDictionary *tempIconCache = [NSMutableDictionary dictionary];
270 NSArray *imageNameArray;
271 NSEnumerator *imageNameEnumerator;
273 NSMutableArray *imageArray;
274 BOOL overlay, looping;
277 //Get the state information
278 overlay = [[stateDict objectForKey:@"Overlay"] boolValue];
279 looping = [[stateDict objectForKey:@"Looping"] boolValue];
280 delay = [[stateDict objectForKey:@"Delay"] floatValue];
281 imageNameArray = [stateDict objectForKey:@"Images"];
282 imageNameEnumerator = [imageNameArray objectEnumerator];
285 imageArray = [NSMutableArray arrayWithCapacity:[imageNameArray count]];
286 while ((imageName = [imageNameEnumerator nextObject])) {
290 #define DOCK_ICON_INTERNAL_PATH @"../Shared Images/"
291 if ([imageName hasPrefix:DOCK_ICON_INTERNAL_PATH]) {
292 //Special hack for all the incorrectly made icon packs we have floating around out there :P
293 imageName = [imageName substringFromIndex:[DOCK_ICON_INTERNAL_PATH length]];
294 imagePath = [[NSBundle mainBundle] pathForResource:[[[imageName stringByDeletingPathExtension] stringByAppendingString:@"-localized"] stringByAppendingPathExtension:[imageName pathExtension]]
296 inDirectory:@"Shared Dock Icon Images"];
299 imagePath = [[NSBundle mainBundle] pathForResource:imageName
301 inDirectory:@"Shared Dock Icon Images"];
305 imagePath = [folderPath stringByAppendingPathComponent:imageName];
308 image = [tempIconCache objectForKey:imagePath]; //We re-use the same images for each state if possible to lower memory usage.
309 if (!image && imagePath) {
310 image = [[[NSImage alloc] initByReferencingFile:imagePath] autorelease];
311 if (image) [tempIconCache setObject:image forKey:imagePath];
314 if (image) [imageArray addObject:image];
318 if (delay != 0 && [imageArray count] != 0) {
319 iconState = [[AIIconState alloc] initWithImages:imageArray
324 NSLog(@"Invalid animated icon state (%@)",imageName);
327 } else { //Static State
333 imageName = [stateDict objectForKey:@"Image"];
335 if ([imageName hasPrefix:DOCK_ICON_INTERNAL_PATH]) {
336 //Special hack for all the incorrectly made icon packs we have floating around out there :P
337 imageName = [imageName substringFromIndex:[DOCK_ICON_INTERNAL_PATH length]];
338 imagePath = [[NSBundle mainBundle] pathForResource:[[[imageName stringByDeletingPathExtension] stringByAppendingString:@"-localized"] stringByAppendingPathExtension:[imageName pathExtension]]
340 inDirectory:@"Shared Dock Icon Images"];
342 imagePath = [[NSBundle mainBundle] pathForResource:imageName
344 inDirectory:@"Shared Dock Icon Images"];
347 imagePath = [folderPath stringByAppendingPathComponent:imageName];
350 //Get the state information
351 image = [[NSImage alloc] initByReferencingFile:imagePath];
352 overlay = [[stateDict objectForKey:@"Overlay"] boolValue];
355 iconState = [[AIIconState alloc] initWithImage:image overlay:overlay];
359 return [iconState autorelease];
362 //Set an icon state from our currently loaded icon pack
363 - (void)setIconStateNamed:(NSString *)inName
365 if (![activeIconStateArray containsObject:inName]) {
366 [activeIconStateArray addObject:inName]; //Add the name to our array
367 [self _setNeedsDisplay]; //Redisplay our icon
371 //Remove an active icon state
372 - (void)removeIconStateNamed:(NSString *)inName
374 if ([activeIconStateArray containsObject:inName]) {
375 [activeIconStateArray removeObject:inName]; //Remove the name from our array
377 [self _setNeedsDisplay]; //Redisplay our icon
382 * @brief Does the current icon know how to display a given state?
384 - (BOOL)currentIconSupportsIconStateNamed:(NSString *)inName
386 return ([[availableIconStateDict objectForKey:@"State"] objectForKey:inName] != nil);
389 //Set a custom icon state
390 - (void)setIconState:(AIIconState *)iconState named:(NSString *)inName
392 [availableDynamicIconStateDict setObject:iconState forKey:inName]; //Add the new state to our available dict
393 [self setIconStateNamed:inName]; //Set it
396 //Build/Pre-render the icon images, start/stop animation
399 NSMutableArray *iconStates = [NSMutableArray array];
400 NSDictionary *availableIcons;
401 NSEnumerator *enumerator;
405 //Stop any existing animation
406 [animationTimer invalidate]; [animationTimer release]; animationTimer = nil;
407 if (observingFlash) {
408 [[adium interfaceController] unregisterFlashObserver:self];
412 //Build an array of the valid active icon states
413 availableIcons = [availableIconStateDict objectForKey:@"State"];
414 enumerator = [activeIconStateArray objectEnumerator];
415 while ((name = [enumerator nextObject])) {
416 if ((state = [availableIcons objectForKey:name]) || (state = [availableDynamicIconStateDict objectForKey:name])) {
417 [iconStates addObject:state];
421 //Generate the composited icon state
422 [currentIconState release];
423 currentIconState = [[AIIconState alloc] initByCompositingStates:iconStates];
426 if (![currentIconState animated]) { //Static icon
427 NSImage *image = [currentIconState image];
429 [[NSApplication sharedApplication] setApplicationIconImage:image];
432 } else { //Animated icon
433 //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.
434 [[adium interfaceController] registerFlashObserver:self];
435 observingFlash = YES;
437 //Set the first frame of our animation
438 [self animateIcon:nil]; //Set the icon and move to the next frame
444 - (void)flash:(int)value
446 //Start the flash timer
447 animationTimer = [[NSTimer scheduledTimerWithTimeInterval:[currentIconState animationDelay]
449 selector:@selector(animateIcon:)
451 repeats:YES] retain];
454 [self animateIcon:animationTimer]; //Set the icon and move to the next frame
456 //Once our animations stops, we no longer need to observe flashing
457 [[adium interfaceController] unregisterFlashObserver:self];
461 //Move the dock to the next animation frame (Assumes the current state is animated)
462 - (void)animateIcon:(NSTimer *)timer
466 //Move to the next image
468 [currentIconState nextFrame];
472 image = [currentIconState image];
474 [[NSApplication sharedApplication] setApplicationIconImage:image];
479 //returns the % of the dock icon's full size that it currently is (0.0 - 1.0)
480 - (float)dockIconScale
482 NSSize trueSize = [[NSScreen mainScreen] visibleFrame].size;
483 NSSize availableSize = [[NSScreen mainScreen] frame].size;
485 int dHeight = availableSize.height - trueSize.height;
486 int dWidth = availableSize.width - trueSize.width;
489 if (dHeight != 22) { //dock is on the bottom
490 if (dHeight == 26) { //dock is hidden
491 } else { //dock is not hidden
492 dockScale = (dHeight-22)/128.0;
494 } else if (dWidth != 0) { //dock is on the side
495 if (dWidth == 4) { //dock is hidden
496 } else { //dock is not hidden
497 dockScale = (dWidth)/128.0;
501 //Add support for multiple monitors
504 if (dockScale <= 0 || dockScale > 1.0) {
512 * @brief Return the dock icon image without any auxiliary states
514 - (NSImage *)baseApplicationIconImage
516 NSDictionary *availableIcons = [availableIconStateDict objectForKey:@"State"];
517 AIIconState *baseState;
518 NSImage *baseApplicationIconImage;
520 if ((baseState = [availableIcons objectForKey:@"Base"])) {
521 AIIconState *iconState = [[[AIIconState alloc] initByCompositingStates:[NSArray arrayWithObject:baseState]] autorelease];
522 baseApplicationIconImage = [iconState image];
524 baseApplicationIconImage = nil;
527 return baseApplicationIconImage;
530 //Bouncing -------------------------------------------------------------------------------------------------------------
531 #pragma mark Bouncing
534 * @brief Perform a bouncing behavior
536 * @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)
538 - (BOOL)performBehavior:(AIDockBehavior)behavior
540 BOOL ongoingBehavior = NO;
542 //Start up the new behavior
544 case AIDockBehaviorStopBouncing: {
545 [self _stopBouncing];
548 case AIDockBehaviorBounceOnce: {
549 if (currentBounceInterval >= SINGLE_BOUNCE_INTERVAL) {
550 currentBounceInterval = SINGLE_BOUNCE_INTERVAL;
551 [self _singleBounce];
555 case AIDockBehaviorBounceRepeatedly: ongoingBehavior = [self _continuousBounce]; break;
556 case AIDockBehaviorBounceDelay_FiveSeconds: ongoingBehavior = [self _bounceWithInterval:5.0]; break;
557 case AIDockBehaviorBounceDelay_TenSeconds: ongoingBehavior = [self _bounceWithInterval:10.0]; break;
558 case AIDockBehaviorBounceDelay_FifteenSeconds: ongoingBehavior = [self _bounceWithInterval:15.0]; break;
559 case AIDockBehaviorBounceDelay_ThirtySeconds: ongoingBehavior = [self _bounceWithInterval:30.0]; break;
560 case AIDockBehaviorBounceDelay_OneMinute: ongoingBehavior = [self _bounceWithInterval:60.0]; break;
564 return ongoingBehavior;
567 //Return a string description of the bouncing behavior
568 - (NSString *)descriptionForBehavior:(AIDockBehavior)behavior
573 case AIDockBehaviorStopBouncing: desc = AILocalizedString(@"None",nil); break;
574 case AIDockBehaviorBounceOnce: desc = AILocalizedString(@"Once",nil); break;
575 case AIDockBehaviorBounceRepeatedly: desc = AILocalizedString(@"Repeatedly",nil); break;
576 case AIDockBehaviorBounceDelay_FiveSeconds: desc = AILocalizedString(@"Every 5 Seconds",nil); break;
577 case AIDockBehaviorBounceDelay_TenSeconds: desc = AILocalizedString(@"Every 10 Seconds",nil); break;
578 case AIDockBehaviorBounceDelay_FifteenSeconds: desc = AILocalizedString(@"Every 15 Seconds",nil); break;
579 case AIDockBehaviorBounceDelay_ThirtySeconds: desc = AILocalizedString(@"Every 30 Seconds",nil); break;
580 case AIDockBehaviorBounceDelay_OneMinute: desc = AILocalizedString(@"Every 60 Seconds",nil); break;
581 default: desc=@""; break;
588 * @brief Start a delayed, repeated bounce
590 * @result YES if we are now bouncing more frequently than before; NO if this call had no effect
592 - (BOOL)_bounceWithInterval:(NSTimeInterval)delay
594 BOOL ongoingBehavior;
596 //Bounce only if the new delay is a faster bounce than the current one
597 if (delay < currentBounceInterval) {
598 [self _singleBounce]; // do one right away
600 currentBounceInterval = delay;
602 bounceTimer = [[NSTimer scheduledTimerWithTimeInterval:delay
604 selector:@selector(bounceWithTimer:)
606 repeats:YES] retain];
608 ongoingBehavior = YES;
610 ongoingBehavior = NO;
613 return ongoingBehavior;
616 //Activated by the time after each delay
617 - (void)bounceWithTimer:(NSTimer *)timer
620 [self _singleBounce];
623 //Bounce once via NSApp's NSInformationalRequest (also used by the timer to perform a single bounce)
624 - (void)_singleBounce
626 if ([NSApp respondsToSelector:@selector(requestUserAttention:)]) {
627 currentAttentionRequest = [NSApp requestUserAttention:NSInformationalRequest];
632 * @brief Bounce continuously via NSApp's NSCriticalRequest
634 * We will bounce until we become the active application or our dock icon is clicked
636 * @result YES if we are now bouncing more frequently than before; NO if this call had no effect
638 - (BOOL)_continuousBounce
640 BOOL ongoingBehavior;
642 if (CONTINUOUS_BOUNCE_INTERVAL < currentBounceInterval) {
643 currentBounceInterval = CONTINUOUS_BOUNCE_INTERVAL;
644 if ([NSApp respondsToSelector:@selector(requestUserAttention:)]) {
645 currentAttentionRequest = [NSApp requestUserAttention:NSCriticalRequest];
648 ongoingBehavior = YES;
650 ongoingBehavior = NO;
653 return ongoingBehavior;
657 - (void)_stopBouncing
661 [bounceTimer invalidate]; [bounceTimer release]; bounceTimer = nil;
664 //Stop any continuous bouncing
665 if (currentAttentionRequest != -1) {
666 if ([NSApp respondsToSelector:@selector(cancelUserAttentionRequest:)]) {
667 [NSApp cancelUserAttentionRequest:currentAttentionRequest];
669 currentAttentionRequest = -1;
672 currentBounceInterval = NO_BOUNCE_INTERVAL;
676 - (void)appWillChangeActive:(NSNotification *)notification
678 [self _stopBouncing]; //Stop any bouncing