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 "AIAppearancePreferencesPlugin.h"
20 #import "AIDockController.h"
21 #import "AIInterfaceController.h"
22 #import "AIPreferenceController.h"
23 #import "ESDebugController.h"
24 #import <AIUtilities/AIDictionaryAdditions.h>
25 #import <AIUtilities/AIFileManagerAdditions.h>
26 #import <AIUtilities/CBApplicationAdditions.h>
27 #import <Adium/AIIconState.h>
28 #import <Adium/IconFamily.h>
30 #define DOCK_DEFAULT_PREFS @"DockPrefs"
31 #define ICON_DISPLAY_DELAY 0.1
33 #define LAST_ICON_UPDATE_VERSION @"Adium:Last Icon Update Version"
35 #define CONTINUOUS_BOUNCE_INTERVAL 0
36 #define SINGLE_BOUNCE_INTERVAL 999
37 #define NO_BOUNCE_INTERVAL 1000
39 @interface AIDockController (PRIVATE)
40 - (void)_setNeedsDisplay;
42 - (void)animateIcon:(NSTimer *)timer;
43 - (void)_singleBounce;
44 - (void)_continuousBounce;
45 - (void)_stopBouncing;
46 - (void)_bounceWithInterval:(double)delay;
47 - (void)preferencesChanged:(NSNotification *)notification;
48 - (AIIconState *)iconStateFromStateDict:(NSDictionary *)stateDict folderPath:(NSString *)folderPath;
49 - (void)updateAppBundleIcon;
53 @interface NSWorkspace (NewTigerMethod)
54 - (void)setIcon:(NSImage *)icon forFile:(NSString *)file options:(int)options;
58 @implementation AIDockController
61 - (void)initController
64 activeIconStateArray = [[NSMutableArray alloc] initWithObjects:@"Base",nil];
65 availableDynamicIconStateDict = [[NSMutableDictionary alloc] init];
66 currentIconState = nil;
67 currentAttentionRequest = -1;
68 currentBounceInterval = NO_BOUNCE_INTERVAL;
73 AIPreferenceController *preferenceController = [adium preferenceController];
74 NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
76 //Register our default preferences
77 [preferenceController registerDefaults:[NSDictionary dictionaryNamed:DOCK_DEFAULT_PREFS
78 forClass:[self class]]
79 forGroup:PREF_GROUP_APPEARANCE];
81 //Observe pref changes
82 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
84 //We always want to stop bouncing when Adium is made active
85 [notificationCenter addObserver:self
86 selector:@selector(appWillChangeActive:)
87 name:NSApplicationWillBecomeActiveNotification
90 //We also stop bouncing when Adium is no longer active
91 [notificationCenter addObserver:self
92 selector:@selector(appWillChangeActive:)
93 name:NSApplicationWillResignActiveNotification
96 //If Adium has been upgraded since the last time we ran, re-apply the user's custom icon
97 NSString *lastVersion = [[NSUserDefaults standardUserDefaults] objectForKey:LAST_ICON_UPDATE_VERSION];
98 if(![[NSApp applicationVersion] isEqualToString:lastVersion]){
99 [self updateAppBundleIcon];
100 [[NSUserDefaults standardUserDefaults] setObject:[NSApp applicationVersion] forKey:LAST_ICON_UPDATE_VERSION];
104 - (void)closeController
106 [[adium preferenceController] unregisterPreferenceObserver:self];
108 NSArray *stateArrayCopy;
109 NSEnumerator *enumerator;
112 //Reset our icon by removing all icon states (except for the base state)
113 stateArrayCopy = [activeIconStateArray copy]; //Work with a copy, since this array will change as we remove states
114 enumerator = [stateArrayCopy objectEnumerator];
115 [enumerator nextObject]; //Skip the first icon
116 while(iconState = [enumerator nextObject]){
117 [self removeIconStateNamed:iconState];
120 //Force the icon to update
123 [stateArrayCopy release];
128 * @brief Returns an array of available dock icon pack paths
130 - (NSArray *)availableDockIconPacks
132 return ([adium allResourcesForName:FOLDER_DOCK_ICONS withExtensions:@"AdiumIcon"]);
137 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
138 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
140 if(!key || [key isEqualToString:KEY_ACTIVE_DOCK_ICON]){
141 NSMutableDictionary *newAvailableIconStateDict;
144 //Load the new icon pack
145 iconPath = [adium pathOfPackWithName:[prefDict objectForKey:KEY_ACTIVE_DOCK_ICON]
146 extension:@"AdiumIcon"
147 resourceFolderName:FOLDER_DOCK_ICONS];
150 if(newAvailableIconStateDict = [[self iconPackAtPath:iconPath] retain]){
151 [availableIconStateDict release]; availableIconStateDict = newAvailableIconStateDict;
155 //Write the icon to the Adium application bundle so finder will see it
156 //On launch we only need to update the icon file if this is a new version of Adium. When preferences
157 //change we always want to update it
159 [self updateAppBundleIcon];
162 //Recomposite the icon
163 [self _setNeedsDisplay];
167 - (void)updateAppBundleIcon
171 image = [[[availableIconStateDict objectForKey:@"State"] objectForKey:@"Base"] image];
173 if([NSApp isOnTigerOrBetter]){
174 [[NSWorkspace sharedWorkspace] setIcon:image
175 forFile:[[NSBundle mainBundle] bundlePath]
179 NSString *icnsPath = [[NSBundle mainBundle] pathForResource:@"Adium" ofType:@"icns"];
180 IconFamily *iconFamily;
182 iconFamily = [IconFamily iconFamilyWithThumbnailsOfImage:image
183 usingImageInterpolation:NSImageInterpolationLow];
184 [iconFamily setAsCustomIconForFile:[[NSBundle mainBundle] bundlePath]];
185 [iconFamily writeToFile:icnsPath];
189 //Finder won't update Adium's icon to match the new one until it is restarted if we don't
190 //tell NSWorkspace to note the change.
191 [[NSWorkspace sharedWorkspace] noteFileSystemChanged:[[NSBundle mainBundle] bundlePath]];
195 //Icons ------------------------------------------------------------------------------------
196 - (void)_setNeedsDisplay
201 //Invoke a display after a short delay
202 [NSTimer scheduledTimerWithTimeInterval:ICON_DISPLAY_DELAY
204 selector:@selector(_buildIcon)
211 - (NSMutableDictionary *)iconPackAtPath:(NSString *)folderPath
213 NSMutableDictionary *iconStateDict;
214 NSDictionary *iconPackDict;
215 NSEnumerator *stateNameKeyEnumerator;
216 NSString *stateNameKey;
219 iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
221 //Process each state in the icon pack, adding it to the iconStateDict
222 iconStateDict = [NSMutableDictionary dictionary];
224 stateNameKeyEnumerator = [[[iconPackDict objectForKey:@"State"] allKeys] objectEnumerator];
225 while((stateNameKey = [stateNameKeyEnumerator nextObject])){
226 NSDictionary *stateDict;
227 AIIconState *iconState;
229 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:stateNameKey];
230 if(iconState = [self iconStateFromStateDict:stateDict folderPath:folderPath]){
231 [iconStateDict setObject:iconState forKey:stateNameKey];
235 return([NSMutableDictionary dictionaryWithObjectsAndKeys:[iconPackDict objectForKey:@"Description"], @"Description", iconStateDict, @"State", nil]);
238 - (AIIconState *)previewStateForIconPackAtPath:(NSString *)folderPath
240 NSDictionary *iconPackDict;
241 NSDictionary *stateDict;
244 iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
246 //Load the preview state
247 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:@"Preview"];
249 return ([self iconStateFromStateDict:stateDict folderPath:folderPath]);
252 - (AIIconState *)iconStateFromStateDict:(NSDictionary *)stateDict folderPath:(NSString *)folderPath
254 AIIconState *iconState = nil;
256 if([[stateDict objectForKey:@"Animated"] intValue]){ //Animated State
257 NSMutableDictionary *tempIconCache = [NSMutableDictionary dictionary];
258 NSArray *imageNameArray;
259 NSEnumerator *imageNameEnumerator;
261 NSMutableArray *imageArray;
262 BOOL overlay, looping;
265 //Get the state information
266 overlay = [[stateDict objectForKey:@"Overlay"] intValue];
267 looping = [[stateDict objectForKey:@"Looping"] intValue];
268 delay = [[stateDict objectForKey:@"Delay"] floatValue];
269 imageNameArray = [stateDict objectForKey:@"Images"];
270 imageNameEnumerator = [imageNameArray objectEnumerator];
273 imageArray = [NSMutableArray arrayWithCapacity:[imageNameArray count]];
274 while((imageName = [imageNameEnumerator nextObject])){
278 #define DOCK_ICON_INTERNAL_PATH @"../Shared Images/"
279 if([imageName hasPrefix:DOCK_ICON_INTERNAL_PATH]){
280 //Special hack for all the incorrectly made icon packs we have floating around out there :P
281 imageName = [imageName substringFromIndex:[DOCK_ICON_INTERNAL_PATH length]];
282 imagePath = [[NSBundle mainBundle] pathForResource:imageName
284 inDirectory:@"Shared Dock Icon Images"];
286 imagePath = [folderPath stringByAppendingPathComponent:imageName];
289 image = [tempIconCache objectForKey:imagePath]; //We re-use the same images for each state if possible to lower memory usage.
290 if(!image && imagePath){
291 image = [[[NSImage alloc] initByReferencingFile:imagePath] autorelease];
292 if(image) [tempIconCache setObject:image forKey:imagePath];
295 if(image) [imageArray addObject:image];
299 if(delay != 0 && [imageArray count] != 0){
300 iconState = [[AIIconState alloc] initWithImages:imageArray
305 NSLog(@"Invalid animated icon state (%@)",imageName);
308 }else{ //Static State
314 imageName = [stateDict objectForKey:@"Image"];
316 if([imageName hasPrefix:DOCK_ICON_INTERNAL_PATH]){
317 //Special hack for all the incorrectly made icon packs we have floating around out there :P
318 imageName = [imageName substringFromIndex:[DOCK_ICON_INTERNAL_PATH length]];
319 imagePath = [[NSBundle mainBundle] pathForResource:imageName
321 inDirectory:@"Shared Dock Icon Images"];
323 imagePath = [folderPath stringByAppendingPathComponent:imageName];
326 //Get the state information
327 image = [[NSImage alloc] initByReferencingFile:imagePath];
328 overlay = [[stateDict objectForKey:@"Overlay"] intValue];
331 iconState = [[AIIconState alloc] initWithImage:image overlay:overlay];
335 return [iconState autorelease];
338 //Set an icon state from our currently loaded icon pack
339 - (void)setIconStateNamed:(NSString *)inName
341 if(![activeIconStateArray containsObject:inName]){
342 [activeIconStateArray addObject:inName]; //Add the name to our array
343 [self _setNeedsDisplay]; //Redisplay our icon
347 //Remove an active icon state
348 - (void)removeIconStateNamed:(NSString *)inName
350 if([activeIconStateArray containsObject:inName]){
351 [activeIconStateArray removeObject:inName]; //Remove the name from our array
353 [self _setNeedsDisplay]; //Redisplay our icon
358 * @brief Does the current icon know how to display a given state?
360 - (BOOL)currentIconSupportsIconStateNamed:(NSString *)inName
362 return ([[availableIconStateDict objectForKey:@"State"] objectForKey:inName] != nil);
365 //Set a custom icon state
366 - (void)setIconState:(AIIconState *)iconState named:(NSString *)inName
368 [availableDynamicIconStateDict setObject:iconState forKey:inName]; //Add the new state to our available dict
369 [self setIconStateNamed:inName]; //Set it
372 //Build/Pre-render the icon images, start/stop animation
375 NSMutableArray *iconStates = [NSMutableArray array];
376 NSDictionary *availableIcons;
377 NSEnumerator *enumerator;
381 //Stop any existing animation
382 [animationTimer invalidate]; [animationTimer release]; animationTimer = nil;
384 [[adium interfaceController] unregisterFlashObserver:self];
388 //Build an array of the valid active icon states
389 availableIcons = [availableIconStateDict objectForKey:@"State"];
390 enumerator = [activeIconStateArray objectEnumerator];
391 while(name = [enumerator nextObject]){
392 if((state = [availableIcons objectForKey:name]) || (state = [availableDynamicIconStateDict objectForKey:name])){
393 [iconStates addObject:state];
397 //Generate the composited icon state
398 [currentIconState release];
399 currentIconState = [[AIIconState alloc] initByCompositingStates:iconStates];
402 if(![currentIconState animated]){ //Static icon
403 NSImage *image = [currentIconState image];
405 [[NSApplication sharedApplication] setApplicationIconImage:image];
408 }else{ //Animated icon
409 //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.
410 [[adium interfaceController] registerFlashObserver:self];
411 observingFlash = YES;
413 //Set the first frame of our animation
414 [self animateIcon:nil]; //Set the icon and move to the next frame
420 - (void)flash:(int)value
422 //Start the flash timer
423 animationTimer = [[NSTimer scheduledTimerWithTimeInterval:[currentIconState animationDelay]
425 selector:@selector(animateIcon:)
427 repeats:YES] retain];
430 [self animateIcon:animationTimer]; //Set the icon and move to the next frame
432 //Once our animations stops, we no longer need to observe flashing
433 [[adium interfaceController] unregisterFlashObserver:self];
437 //Move the dock to the next animation frame (Assumes the current state is animated)
438 - (void)animateIcon:(NSTimer *)timer
442 //Move to the next image
444 [currentIconState nextFrame];
448 image = [currentIconState image];
450 [[NSApplication sharedApplication] setApplicationIconImage:image];
455 //returns the % of the dock icon's full size that it currently is (0.0 - 1.0)
456 - (float)dockIconScale
458 NSSize trueSize = [[NSScreen mainScreen] visibleFrame].size;
459 NSSize availableSize = [[NSScreen mainScreen] frame].size;
461 int dHeight = availableSize.height - trueSize.height;
462 int dWidth = availableSize.width - trueSize.width;
465 if(dHeight != 22){ //dock is on the bottom
466 if(dHeight == 26){ //dock is hidden
467 }else{ //dock is not hidden
468 dockScale = (dHeight-22)/128.0;
470 }else if(dWidth != 0){ //dock is on the side
471 if(dWidth == 4){ //dock is hidden
472 }else{ //dock is not hidden
473 dockScale = (dWidth)/128.0;
477 //Add support for multiple monitors
480 if(dockScale <= 0 || dockScale > 1.0){
488 //Bouncing -------------------------------------------------------------------------------------------------------------
489 #pragma mark Bouncing
491 //Perform a bouncing behavior
492 - (void)performBehavior:(DOCK_BEHAVIOR)behavior
494 //Start up the new behavior
497 [self _stopBouncing];
501 if (currentBounceInterval >= SINGLE_BOUNCE_INTERVAL){
502 currentBounceInterval = SINGLE_BOUNCE_INTERVAL;
503 [self _singleBounce];
507 case BOUNCE_REPEAT: [self _continuousBounce]; break;
508 case BOUNCE_DELAY5: [self _bounceWithInterval:5.0]; break;
509 case BOUNCE_DELAY10: [self _bounceWithInterval:10.0]; break;
510 case BOUNCE_DELAY15: [self _bounceWithInterval:15.0]; break;
511 case BOUNCE_DELAY30: [self _bounceWithInterval:30.0]; break;
512 case BOUNCE_DELAY60: [self _bounceWithInterval:60.0]; break;
517 //Return a string description of the bouncing behavior
518 - (NSString *)descriptionForBehavior:(DOCK_BEHAVIOR)behavior
523 case BOUNCE_NONE: desc = AILocalizedString(@"None",nil); break;
524 case BOUNCE_ONCE: desc = AILocalizedString(@"Once",nil); break;
525 case BOUNCE_REPEAT: desc = AILocalizedString(@"Repeatedly",nil); break;
526 case BOUNCE_DELAY5: desc = AILocalizedString(@"Every 5 Seconds",nil); break;
527 case BOUNCE_DELAY10: desc = AILocalizedString(@"Every 10 Seconds",nil); break;
528 case BOUNCE_DELAY15: desc = AILocalizedString(@"Every 15 Seconds",nil); break;
529 case BOUNCE_DELAY30: desc = AILocalizedString(@"Every 30 Seconds",nil); break;
530 case BOUNCE_DELAY60: desc = AILocalizedString(@"Every 60 Seconds",nil); break;
531 default: desc=@""; break;
537 //Start a delayed bounce
538 - (void)_bounceWithInterval:(NSTimeInterval)delay
540 //Bounce only if the new delay is a faster bounce than the current one
541 if (delay < currentBounceInterval){
542 [self _singleBounce]; // do one right away
544 currentBounceInterval = delay;
546 bounceTimer = [[NSTimer scheduledTimerWithTimeInterval:delay
548 selector:@selector(bounceWithTimer:)
550 repeats:YES] retain];
554 //Activated by the time after each delay
555 - (void)bounceWithTimer:(NSTimer *)timer
558 [self _singleBounce];
561 //Bounce once via NSApp's NSInformationalRequest (also used by the timer to perform a single bounce)
562 - (void)_singleBounce
564 if([NSApp respondsToSelector:@selector(requestUserAttention:)]){
565 currentAttentionRequest = [NSApp requestUserAttention:NSInformationalRequest];
569 //Bounce continuously via NSApp's NSCriticalRequest
570 - (void)_continuousBounce
572 if (CONTINUOUS_BOUNCE_INTERVAL < currentBounceInterval){
573 currentBounceInterval = CONTINUOUS_BOUNCE_INTERVAL;
574 if([NSApp respondsToSelector:@selector(requestUserAttention:)]){
575 currentAttentionRequest = [NSApp requestUserAttention:NSCriticalRequest];
581 - (void)_stopBouncing
585 [bounceTimer invalidate]; [bounceTimer release]; bounceTimer = nil;
588 //Stop any continuous bouncing
589 if(currentAttentionRequest != -1){
590 if([NSApp respondsToSelector:@selector(cancelUserAttentionRequest:)]){
591 [NSApp cancelUserAttentionRequest:currentAttentionRequest];
593 currentAttentionRequest = -1;
596 currentBounceInterval = NO_BOUNCE_INTERVAL;
600 - (void)appWillChangeActive:(NSNotification *)notification
602 [self _stopBouncing]; //Stop any bouncing