Request info to get user icons when a Jabber contact signs on. Fixes #4205
[adiumx.git] / Source / AIDockController.m
blob76f0cbb9c55cca0cddcb3e3fb4c29e6397944409
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
17 // $Id$
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;
39 - (void)_buildIcon;
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;
48 @end
50 #ifndef MAC_OS_X_10_4
51 @interface NSWorkspace (NewTigerMethod)
52 - (BOOL)setIcon:(NSImage *)image forFile:(NSString *)fullPath options:(unsigned)options;
53 @end
54 #endif
56 @implementation AIDockController
58 //init and close
59 - (id)init
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;
67                 animationTimer = nil;
68                 bounceTimer = nil;
69                 needsDisplay = NO;
70         }
71         
72         return self;
75 - (void)controllerDidLoad
77         NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
78         
79     //Register our default preferences
80     [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:DOCK_DEFAULT_PREFS
81                                                                                                                                                 forClass:[self class]] 
82                                                                                   forGroup:PREF_GROUP_APPEARANCE];
83     
84     //Observe pref changes
85         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
86         
87     //We always want to stop bouncing when Adium is made active
88     [notificationCenter addObserver:self
89                                selector:@selector(appWillChangeActive:) 
90                                    name:NSApplicationWillBecomeActiveNotification 
91                                  object:nil];
92         
93     //We also stop bouncing when Adium is no longer active
94     [notificationCenter addObserver:self
95                                selector:@selector(appWillChangeActive:) 
96                                    name:NSApplicationWillResignActiveNotification 
97                                  object:nil];
98         
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];
104         }
107 - (void)controllerWillClose
109         [[adium preferenceController] unregisterPreferenceObserver:self];
111         NSArray                 *stateArrayCopy;
112         NSEnumerator    *enumerator;
113         NSString                *iconState;
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];
121         }
123         //Force the icon to update
124         [self _buildIcon];
126         [stateArrayCopy release];
131  * @brief Returns an array of available dock icon pack paths
132  */
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
137         NSString * path;
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];
144         }
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;
155                 NSString                        *iconPath;
156                 
157                 //Load the new icon pack
158                 iconPath = [adium pathOfPackWithName:[prefDict objectForKey:KEY_ACTIVE_DOCK_ICON]
159                                                                    extension:@"AdiumIcon"
160                                                   resourceFolderName:FOLDER_DOCK_ICONS];
162                 if (iconPath) {
163                         if ((newAvailableIconStateDict = [[self iconPackAtPath:iconPath] retain])) {
164                                 [availableIconStateDict release]; availableIconStateDict = newAvailableIconStateDict;
165                         }
166                 }
167                 
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
171                 if (!firstTime) {
172                         [self updateAppBundleIcon];
173                 }
175                 //Recomposite the icon
176                 [self _setNeedsDisplay];
177         }
180 - (void)updateAppBundleIcon
182         NSImage                 *image;
183         
184         image = [[[availableIconStateDict objectForKey:@"State"] objectForKey:@"Base"] image];
185         if (image) {
186                 if ([NSApp isOnTigerOrBetter]) {
187                         [[NSWorkspace sharedWorkspace] setIcon:image 
188                                                                                    forFile:[[NSBundle mainBundle] bundlePath]
189                                                                                    options:0];
190                         
191                 } else {
192                         NSString                *icnsPath = [[NSBundle mainBundle] pathForResource:@"Adium" ofType:@"icns"];
193                         IconFamily              *iconFamily;
194         
195                         iconFamily = [IconFamily iconFamilyWithThumbnailsOfImage:image
196                                                                                          usingImageInterpolation:NSImageInterpolationLow];
197                         [iconFamily setAsCustomIconForFile:[[NSBundle mainBundle] bundlePath]];
198                         [iconFamily writeToFile:icnsPath];
200                 }
201                 
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]];
205         }
208 //Icons ------------------------------------------------------------------------------------
209 - (void)_setNeedsDisplay
211     if (!needsDisplay) {
212         needsDisplay = YES;
214         //Invoke a display after a short delay
215         [NSTimer scheduledTimerWithTimeInterval:ICON_DISPLAY_DELAY
216                                          target:self
217                                        selector:@selector(_buildIcon)
218                                        userInfo:nil
219                                         repeats:NO];
220     }
223 //Load an icon pack
224 - (NSMutableDictionary *)iconPackAtPath:(NSString *)folderPath
226     NSMutableDictionary *iconStateDict;
227     NSDictionary                *iconPackDict;
228     NSEnumerator                *stateNameKeyEnumerator;
229     NSString                    *stateNameKey;
231     //Load the icon pack
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];
236         
237     stateNameKeyEnumerator = [[[iconPackDict objectForKey:@"State"] allKeys] objectEnumerator];
238     while ((stateNameKey = [stateNameKeyEnumerator nextObject])) {
239                 NSDictionary    *stateDict;
240                 AIIconState             *iconState;
241                 
242                 stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:stateNameKey];
243                 if ((iconState = [self iconStateFromStateDict:stateDict folderPath:folderPath])) {
244                         [iconStateDict setObject:iconState forKey:stateNameKey];
245                 }
246         }
247         
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
257  */
258 - (void)getName:(NSString **)outName previewState:(AIIconState **)outIconState forIconPackAtPath:(NSString *)folderPath
260         NSDictionary    *iconPackDict;
261         NSDictionary    *stateDict;
262         
263         //Load the icon pack
264     iconPackDict = [NSDictionary dictionaryWithContentsOfFile:[folderPath stringByAppendingPathComponent:@"IconPack.plist"]];
265         
266         //Load the preview state
267         stateDict = [[iconPackDict objectForKey:@"State"] objectForKey:@"Preview"];
268         
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;
276         
277         [self getName:NULL previewState:&previewState forIconPackAtPath:folderPath];
278         
279         return previewState;
282 - (AIIconState *)iconStateFromStateDict:(NSDictionary *)stateDict folderPath:(NSString *)folderPath
284         AIIconState             *iconState = nil;
285         
286         if ([[stateDict objectForKey:@"Animated"] intValue]) { //Animated State
287                 NSMutableDictionary     *tempIconCache = [NSMutableDictionary dictionary];
288                 NSArray                         *imageNameArray;
289                 NSEnumerator            *imageNameEnumerator;
290                 NSString                        *imageName;
291                 NSMutableArray          *imageArray;
292                 BOOL                            overlay, looping;
293                 float                           delay;
294                 
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];
302                 //Load the images
303                 imageArray = [NSMutableArray arrayWithCapacity:[imageNameArray count]];
304                 while ((imageName = [imageNameEnumerator nextObject])) {
305                         NSString        *imagePath;
306                         NSImage         *image;
307                         
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]]
313                                                                             ofType:@""
314                                                                        inDirectory:@"Shared Dock Icon Images"];
315                                 
316                                 if (!imagePath) {
317                                         imagePath = [[NSBundle mainBundle] pathForResource:imageName
318                                                                                                                                 ofType:@""
319                                                                                                                    inDirectory:@"Shared Dock Icon Images"];
320                                 }
322                         } else {
323                                 imagePath = [folderPath stringByAppendingPathComponent:imageName];
324                         }
325                         
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];
330                         }
331                         
332                         if (image) [imageArray addObject:image];
333                 }
334                 
335                 //Create the state
336                 if (delay != 0 && [imageArray count] != 0) {
337                         iconState = [[AIIconState alloc] initWithImages:imageArray
338                                                                                                           delay:delay
339                                                                                                         looping:looping
340                                                                                                         overlay:overlay];
341                 } else {
342                         NSLog(@"Invalid animated icon state (%@)",imageName);
343                 }
344                 
345         } else { //Static State
346                 NSString        *imageName;
347                 NSString        *imagePath;
348                 NSImage         *image;
349                 BOOL            overlay;
350                 
351                 imageName = [stateDict objectForKey:@"Image"];
352                 
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]]
357                                                                                                                 ofType:@""
358                                                                                                    inDirectory:@"Shared Dock Icon Images"];
359                         if (!imagePath) {
360                                 imagePath = [[NSBundle mainBundle] pathForResource:imageName
361                                                                     ofType:@""
362                                                                inDirectory:@"Shared Dock Icon Images"];
363                         }
364                 } else {
365                         imagePath = [folderPath stringByAppendingPathComponent:imageName];
366                 }
368                 //Get the state information
369                 image = [[NSImage alloc] initByReferencingFile:imagePath];
370                 overlay = [[stateDict objectForKey:@"Overlay"] boolValue];
371                 
372                 //Create the state
373                 iconState = [[AIIconState alloc] initWithImage:image overlay:overlay];          
374                 [image release];
375         }
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
386     }
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
394         
395         [self _setNeedsDisplay];                        //Redisplay our icon
396     }
400  * @brief Does the current icon know how to display a given state?
401  */
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
415 - (void)_buildIcon
417     NSMutableArray      *iconStates = [NSMutableArray array];
418     NSDictionary        *availableIcons;
419     NSEnumerator        *enumerator;
420     AIIconState         *state;
421     NSString            *name;
423     //Stop any existing animation
424     [animationTimer invalidate]; [animationTimer release]; animationTimer = nil;
425     if (observingFlash) {
426         [[adium interfaceController] unregisterFlashObserver:self];
427         observingFlash = NO;
428     }
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];
436         }
437     }
439     //Generate the composited icon state
440     [currentIconState release];
441     currentIconState = [[AIIconState alloc] initByCompositingStates:iconStates];
443     //
444     if (![currentIconState animated]) { //Static icon
445                 NSImage *image = [currentIconState image];
446         if (image) {
447                          [[NSApplication sharedApplication] setApplicationIconImage:image];
448                 }
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
457     }
459     needsDisplay = NO;
462 - (void)flash:(int)value
464     //Start the flash timer
465     animationTimer = [[NSTimer scheduledTimerWithTimeInterval:[currentIconState animationDelay]
466                                                        target:self
467                                                      selector:@selector(animateIcon:)
468                                                      userInfo:nil
469                                                       repeats:YES] retain];
471     //Animate the icon
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];
476     observingFlash = NO;
479 //Move the dock to the next animation frame (Assumes the current state is animated)
480 - (void)animateIcon:(NSTimer *)timer
482     NSImage     *image;
484     //Move to the next image
485     if (timer) {
486         [currentIconState nextFrame];
487     }
488         
489     //Set the image
490     image = [currentIconState image];
491         if (image) {
492                 [[NSApplication sharedApplication] setApplicationIconImage:image];
493         }
494     
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;
505     float dockScale = 0;
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;
511         }
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;
516         }
517     } else {
518         //multiple monitors?
519         //Add support for multiple monitors
520     }
522     if (dockScale <= 0 || dockScale > 1.0) {
523         dockScale = 0.3;
524     }
526     return dockScale;
530  * @brief Return the dock icon image without any auxiliary states
531  */
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];
541         } else {
542                 baseApplicationIconImage = nil;
543         }
544         
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)
555  */
556 - (BOOL)performBehavior:(AIDockBehavior)behavior
558         BOOL    ongoingBehavior = NO;
560     //Start up the new behavior
561     switch (behavior) {
562         case AIDockBehaviorStopBouncing: {
563                         [self _stopBouncing];
564                         break;
565                 }
566         case AIDockBehaviorBounceOnce: {
567                         if (currentBounceInterval >= SINGLE_BOUNCE_INTERVAL) {
568                                 currentBounceInterval = SINGLE_BOUNCE_INTERVAL;
569                                 [self _singleBounce];
570                         }
571                         break;
572                 }
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;
579         default: break;
580     }
581         
582         return ongoingBehavior;
585 //Return a string description of the bouncing behavior
586 - (NSString *)descriptionForBehavior:(AIDockBehavior)behavior
588         NSString        *desc;
589         
590     switch (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;
600     }    
602         return desc;
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
609  */
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
617                 
618                 currentBounceInterval = delay;
619                 
620                 bounceTimer = [[NSTimer scheduledTimerWithTimeInterval:delay
621                                                                                                                 target:self
622                                                                                                           selector:@selector(bounceWithTimer:)
623                                                                                                           userInfo:nil
624                                                                                                            repeats:YES] retain];
625                 
626                 ongoingBehavior = YES;
627         } else {
628                 ongoingBehavior = NO;
629         }
630         
631         return ongoingBehavior;
634 //Activated by the time after each delay
635 - (void)bounceWithTimer:(NSTimer *)timer
637     //Bounce
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];
646     }
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
655  */
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];
664                 }
666                 ongoingBehavior = YES;
667         } else {
668                 ongoingBehavior = NO;   
669         }
670         
671         return ongoingBehavior;
674 //Stop bouncing
675 - (void)_stopBouncing
677     //Stop any timer
678     if (bounceTimer) {
679         [bounceTimer invalidate]; [bounceTimer release]; bounceTimer = nil;
680     }
682     //Stop any continuous bouncing
683     if (currentAttentionRequest != -1) {
684         if ([NSApp respondsToSelector:@selector(cancelUserAttentionRequest:)]) {
685             [NSApp cancelUserAttentionRequest:currentAttentionRequest];
686         }
687         currentAttentionRequest = -1;
688     }
689         
690         currentBounceInterval = NO_BOUNCE_INTERVAL;
694 - (void)appWillChangeActive:(NSNotification *)notification
696     [self _stopBouncing]; //Stop any bouncing
699 @end