Plugins/Contact Status Dock Overlays/AIContactStatusDockOverlaysPlugin.m
changeset 3670 add0c83648a5
parent 3669 7905c08bc4d8
child 3671 855cbc039a80
equal deleted inserted replaced
3669:7905c08bc4d8 3670:add0c83648a5
     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  */
       
    16 
       
    17 #import "AIContactStatusDockOverlaysPlugin.h"
       
    18 #import <Adium/AIChatControllerProtocol.h>
       
    19 #import <Adium/AIContactControllerProtocol.h>
       
    20 #import <Adium/AIContentControllerProtocol.h>
       
    21 #import "AIDockController.h"
       
    22 #import <Adium/AIInterfaceControllerProtocol.h>
       
    23 #import <Adium/AIContactAlertsControllerProtocol.h>
       
    24 #import <AIUtilities/AIColorAdditions.h>
       
    25 #import <AIUtilities/AIDictionaryAdditions.h>
       
    26 #import <AIUtilities/AIParagraphStyleAdditions.h>
       
    27 #import <AIUtilities/AIArrayAdditions.h>
       
    28 #import <AIUtilities/AIImageAdditions.h>
       
    29 #import <Adium/AIAbstractListController.h>
       
    30 #import <Adium/AIAccount.h>
       
    31 #import <Adium/AIChat.h>
       
    32 #import <Adium/AIIconState.h>
       
    33 
       
    34 #define SMALLESTRADIUS				15
       
    35 #define RADIUSRANGE					36
       
    36 #define SMALLESTFONTSIZE			14
       
    37 #define FONTSIZERANGE				30
       
    38 
       
    39 #define	DOCK_OVERLAY_ALERT_SHORT	AILocalizedString(@"Display name in the dock icon",nil)
       
    40 #define DOCK_OVERLAY_ALERT_LONG		DOCK_OVERLAY_ALERT_SHORT
       
    41 
       
    42 @interface AIContactStatusDockOverlaysPlugin ()
       
    43 - (void)_setOverlay;
       
    44 - (NSImage *)overlayImageFlash:(BOOL)flash;
       
    45 - (void)flushPreferenceColorCache;
       
    46 - (void)chatClosed:(NSNotification *)notification;
       
    47 - (void)removeDockOverlay:(NSTimer *)removeTimer;
       
    48 @end
       
    49 
       
    50 @implementation AIContactStatusDockOverlaysPlugin
       
    51 
       
    52 /*!
       
    53 * @brief Install
       
    54  */
       
    55 - (void)installPlugin
       
    56 {
       
    57 	overlayObjectsArray = [[NSMutableArray alloc] init];
       
    58     overlayState = nil;
       
    59 
       
    60     //Register as a contact observer (For signed on / signed off)
       
    61     [[AIContactObserverManager sharedManager] registerListObjectObserver:self];
       
    62 	
       
    63 	//Register as a chat observer (for unviewed content)
       
    64 	[adium.chatController registerChatObserver:self];
       
    65 	
       
    66 	[[NSNotificationCenter defaultCenter] addObserver:self
       
    67 								   selector:@selector(chatClosed:)
       
    68 									   name:Chat_WillClose
       
    69 									 object:nil];
       
    70 	
       
    71     //Prefs
       
    72 	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
       
    73 	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
       
    74 	
       
    75     //
       
    76     image1 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
       
    77     image2 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
       
    78 	
       
    79 	//Install our contact alert
       
    80 	[adium.contactAlertsController registerActionID:DOCK_OVERLAY_ALERT_IDENTIFIER withHandler:self];
       
    81 }
       
    82 
       
    83 - (void)uninstallPlugin
       
    84 {
       
    85 	[[AIContactObserverManager sharedManager] unregisterListObjectObserver:self];
       
    86 	[adium.chatController unregisterChatObserver:self];
       
    87 	[[NSNotificationCenter defaultCenter] removeObserver:self];
       
    88 	[adium.preferenceController unregisterPreferenceObserver:self];
       
    89 }
       
    90 
       
    91 /*!
       
    92 * @brief Short description
       
    93  * @result A short localized description of the action
       
    94  */
       
    95 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
       
    96 {
       
    97 	return DOCK_OVERLAY_ALERT_SHORT;
       
    98 }
       
    99 
       
   100 /*!
       
   101 * @brief Long description
       
   102  * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
       
   103  */
       
   104 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
       
   105 {
       
   106 	return DOCK_OVERLAY_ALERT_LONG;
       
   107 }
       
   108 
       
   109 /*!
       
   110 * @brief Image
       
   111  */
       
   112 - (NSImage *)imageForActionID:(NSString *)actionID
       
   113 {
       
   114 	//XXX
       
   115 	return [NSImage imageNamed:@"DockAlert" forClass:[self class]];
       
   116 }
       
   117 
       
   118 /*!
       
   119  * @brief Details pane
       
   120  * @result An <tt>AIModularPane</tt> to use for configuring this action, or nil if no configuration is possible.
       
   121  */
       
   122 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
       
   123 {
       
   124 	return nil;
       
   125 }
       
   126 
       
   127 /*!
       
   128 * @brief Perform an action
       
   129  *
       
   130  * @param actionID The ID of the action to perform
       
   131  * @param listObject The listObject associated with the event triggering the action. It may be nil
       
   132  * @param details If set by the details pane when the action was created, the details dictionary for this particular action
       
   133  * @param eventID The eventID which triggered this action
       
   134  * @param userInfo Additional information associated with the event; userInfo's type will vary with the actionID.
       
   135  */
       
   136 - (BOOL)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
       
   137 {
       
   138 	BOOL isMessageEvent = [adium.contactAlertsController isMessageEvent:eventID];
       
   139 	
       
   140 	if (isMessageEvent) {
       
   141 		AIChat	*chat;
       
   142 
       
   143 		if ((chat = [userInfo objectForKey:@"AIChat"]) &&
       
   144 		   (chat != adium.interfaceController.activeChat) &&
       
   145 		   (![overlayObjectsArray containsObjectIdenticalTo:chat])) {
       
   146 			[overlayObjectsArray addObject:chat];
       
   147 			
       
   148 			//Wait until the next run loop so this event is done processing (and our unviewed content count is right)
       
   149 			[self performSelector:@selector(_setOverlay)
       
   150 					   withObject:nil
       
   151 					   afterDelay:0];
       
   152 
       
   153 			/* The chat observer method is responsible for removing this overlay later */
       
   154 		}
       
   155 
       
   156 	} else if (listObject) {
       
   157 		NSTimer	*removeTimer;
       
   158 		
       
   159 		//Clear any current timer for this object o ahve its overlay removed
       
   160 		if ((removeTimer = [listObject valueForProperty:@"DockOverlayRemoveTimer"])) [removeTimer invalidate];
       
   161 		
       
   162 		//Add a timer to remove this overlay
       
   163 		removeTimer = [NSTimer scheduledTimerWithTimeInterval:5
       
   164 													   target:self
       
   165 													 selector:@selector(removeDockOverlay:)
       
   166 													 userInfo:listObject
       
   167 													  repeats:NO];
       
   168 		[listObject setValue:removeTimer
       
   169 							 forProperty:@"DockOverlayRemoveTimer"
       
   170 							 notify:NotifyNever];
       
   171 
       
   172 		if (![overlayObjectsArray containsObject:listObject]) {
       
   173 			[overlayObjectsArray addObject:listObject];
       
   174 		}
       
   175 
       
   176 		//Wait until the next run loop so this event is done processing
       
   177 		[self performSelector:@selector(_setOverlay)
       
   178 				   withObject:nil
       
   179 				   afterDelay:0];
       
   180 	}
       
   181 	
       
   182 	return YES;
       
   183 }
       
   184 
       
   185 - (void)removeDockOverlay:(NSTimer *)removeTimer
       
   186 {
       
   187 	AIListObject	*inObject = [removeTimer userInfo];
       
   188 
       
   189 	[overlayObjectsArray removeObjectIdenticalTo:inObject];
       
   190 	
       
   191 	[inObject setValue:nil
       
   192 					   forProperty:@"DockOverlayRemoveTimer"
       
   193 					   notify:NotifyNever];
       
   194 	
       
   195 	[self _setOverlay];
       
   196 }
       
   197 
       
   198 - (void)chatClosed:(NSNotification *)notification
       
   199 {
       
   200 	AIChat	*chat = [notification object];
       
   201 	
       
   202 	[overlayObjectsArray removeObjectIdenticalTo:chat];
       
   203 	
       
   204 	[self _setOverlay];
       
   205 }
       
   206 
       
   207 /*!
       
   208 * @brief Allow multiple actions?
       
   209  *
       
   210  * If this method returns YES, every one of this action associated with the triggering event will be executed.
       
   211  * If this method returns NO, only the first will be.
       
   212  *
       
   213  * Don't allow multiple dock actions to occur.  While a series of "Bounce every 5 seconds," "Bounce every 10 seconds,"
       
   214  * and so on actions could be combined sanely, a series of "Bounce once" would make the dock go crazy.
       
   215  */
       
   216 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
       
   217 {
       
   218 	return NO;
       
   219 }
       
   220 
       
   221 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
       
   222 							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
       
   223 {	
       
   224 	if ([group isEqualToString:PREF_GROUP_LIST_THEME]) {
       
   225 		//Grab colors from status coloring plugin's prefs    
       
   226 		[self flushPreferenceColorCache];
       
   227 		signedOffColor = [[[prefDict objectForKey:KEY_SIGNED_OFF_COLOR] representedColor] retain];
       
   228 		signedOnColor = [[[prefDict objectForKey:KEY_SIGNED_ON_COLOR] representedColor] retain];
       
   229 		unviewedContentColor = [[[prefDict objectForKey:KEY_UNVIEWED_COLOR] representedColor] retain];
       
   230 		
       
   231 		backSignedOffColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_OFF_COLOR] representedColor] retain];
       
   232 		backSignedOnColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_ON_COLOR] representedColor] retain];
       
   233 		backUnviewedContentColor = [[[prefDict objectForKey:KEY_LABEL_UNVIEWED_COLOR] representedColor] retain];
       
   234 
       
   235 	} else if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
       
   236 		if (!key || [key isEqualToString:KEY_ANIMATE_DOCK_ICON]) {
       
   237 			BOOL newShouldAnimate = [[prefDict objectForKey:KEY_ANIMATE_DOCK_ICON] boolValue];
       
   238 			if (newShouldAnimate != shouldAnimate) {
       
   239 				shouldAnimate = newShouldAnimate;
       
   240 
       
   241 				//Redo our overlay to respect our new preference
       
   242 				if (!firstTime) [self _setOverlay];
       
   243 			}
       
   244 		}
       
   245 	}
       
   246 }
       
   247 
       
   248 
       
   249 - (void)flushPreferenceColorCache
       
   250 {
       
   251 	[signedOffColor release]; signedOffColor = nil;
       
   252 	[signedOnColor release]; signedOnColor = nil;
       
   253 	[unviewedContentColor release]; unviewedContentColor = nil;
       
   254 	[backSignedOffColor release]; backSignedOffColor = nil;
       
   255 	[backSignedOnColor release]; backSignedOnColor = nil;
       
   256 	[backUnviewedContentColor release]; backUnviewedContentColor = nil;	
       
   257 }
       
   258 
       
   259 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
       
   260 {
       
   261 	if ([inObject isKindOfClass:[AIAccount class]]) {
       
   262 		//When an account signs on or off, force an overlay update as it may have silently changed
       
   263 		//contacts' statuses
       
   264 		if ([inModifiedKeys containsObject:@"isOnline"]) {
       
   265 			BOOL			madeChanges = NO;
       
   266 			
       
   267 			for (AIListObject *listObject in [[overlayObjectsArray copy] autorelease]) {
       
   268 				if (([listObject respondsToSelector:@selector(account)]) &&
       
   269 				   ([(id)listObject account] == inObject) &&
       
   270 				   ([overlayObjectsArray containsObjectIdenticalTo:listObject])) {
       
   271 					[overlayObjectsArray removeObject:listObject];
       
   272 					madeChanges = YES;
       
   273 				}
       
   274 			}
       
   275 			
       
   276 			if (madeChanges) [self _setOverlay];
       
   277 		}
       
   278 	}
       
   279 	
       
   280 	return nil;
       
   281 }
       
   282 
       
   283 /*!
       
   284  * @brief When a chat no longer has unviewed content, remove it from display
       
   285  */
       
   286 - (NSSet *)updateChat:(AIChat *)inChat keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
       
   287 {
       
   288 	if (inModifiedKeys == nil || [inModifiedKeys containsObject:KEY_UNVIEWED_CONTENT]) {
       
   289 		
       
   290 		if (![inChat unviewedContentCount]) {
       
   291 			if ([overlayObjectsArray containsObjectIdenticalTo:inChat]) {
       
   292 				[overlayObjectsArray removeObjectIdenticalTo:inChat];
       
   293 				[self _setOverlay];
       
   294 			}
       
   295 		}
       
   296 	}
       
   297 	
       
   298 	return nil;
       
   299 }
       
   300 
       
   301 - (void)_setOverlay
       
   302 {
       
   303     //Remove & release the current overlay state
       
   304     if (overlayState) {
       
   305         [adium.dockController removeIconStateNamed:@"ContactStatusOverlay"];
       
   306         [overlayState release]; overlayState = nil;
       
   307     }
       
   308 
       
   309     //Create & set the new overlay state
       
   310     if ([overlayObjectsArray count] != 0) {
       
   311         //Set the state
       
   312 		if (shouldAnimate) {
       
   313 			overlayState = [[AIIconState alloc] initWithImages:[NSArray arrayWithObjects:[self overlayImageFlash:NO], [self overlayImageFlash:YES], nil]
       
   314 														 delay:0.5f
       
   315 												       looping:YES 
       
   316 													   overlay:YES];
       
   317 		} else {
       
   318 			overlayState = [[AIIconState alloc] initWithImage:[self overlayImageFlash:NO]
       
   319 													  overlay:YES];
       
   320 		}
       
   321 
       
   322         [adium.dockController setIconState:overlayState named:@"ContactStatusOverlay"];
       
   323     }   
       
   324 }
       
   325 
       
   326 - (NSImage *)overlayImageFlash:(BOOL)flash
       
   327 {
       
   328     NSEnumerator		*enumerator;
       
   329     ESObjectWithProperties  *object;
       
   330     NSFont				*font;
       
   331     NSParagraphStyle	*paragraphStyle;
       
   332     CGFloat				dockIconScale;
       
   333     CGFloat					iconHeight;
       
   334     CGFloat				top, bottom;
       
   335     NSImage				*image = (flash ? image1 : image2);
       
   336 	
       
   337     //Pre-calc some sizes
       
   338     dockIconScale = 1- [adium.dockController dockIconScale];
       
   339     iconHeight = (SMALLESTRADIUS + (RADIUSRANGE * dockIconScale));
       
   340 
       
   341 	top = 126;
       
   342 	bottom = top - iconHeight;
       
   343 
       
   344     //Set up the string details
       
   345     font = [NSFont boldSystemFontOfSize:(SMALLESTFONTSIZE + (FONTSIZERANGE * dockIconScale))];
       
   346     paragraphStyle = [NSParagraphStyle styleWithAlignment:NSCenterTextAlignment lineBreakMode:NSLineBreakByClipping];
       
   347 	
       
   348     //Clear our image
       
   349     [image lockFocus];
       
   350     [[NSColor clearColor] set];
       
   351     NSRectFillUsingOperation(NSMakeRect(0, 0, 128, 128), NSCompositeCopy);
       
   352 	
       
   353     //Draw overlays for each contact
       
   354     enumerator = [overlayObjectsArray reverseObjectEnumerator];
       
   355     while ((object = [enumerator nextObject]) && !(top < 0) && bottom < 128) {
       
   356         CGFloat			left, right, arcRadius, stringInset;
       
   357         NSBezierPath	*path;
       
   358         NSColor			*backColor = nil, *textColor = nil, *borderColor = nil;
       
   359 		
       
   360         //Create the pill frame
       
   361         arcRadius = (iconHeight / 2.0f);
       
   362         stringInset = (iconHeight / 4.0f);
       
   363         left = 1 + arcRadius;
       
   364         right = 127 - arcRadius;
       
   365 		
       
   366         path = [NSBezierPath bezierPath];
       
   367         [path setLineWidth:((iconHeight/2.0f) * 0.13333f)];
       
   368         //Top
       
   369         [path moveToPoint: NSMakePoint(left, top)];
       
   370         [path lineToPoint: NSMakePoint(right, top)];
       
   371 		
       
   372         //Right rounded cap
       
   373         [path appendBezierPathWithArcWithCenter:NSMakePoint(right, top - arcRadius) 
       
   374 										 radius:arcRadius
       
   375 									 startAngle:90
       
   376 									   endAngle:0
       
   377 									  clockwise:YES];
       
   378         [path lineToPoint: NSMakePoint(right + arcRadius, bottom + arcRadius)];
       
   379         [path appendBezierPathWithArcWithCenter:NSMakePoint(right, bottom + arcRadius) 
       
   380 										 radius:arcRadius
       
   381 									 startAngle:0
       
   382 									   endAngle:270
       
   383 									  clockwise:YES];
       
   384 		
       
   385         //Bottom
       
   386         [path moveToPoint: NSMakePoint(right, bottom)];
       
   387         [path lineToPoint: NSMakePoint(left, bottom)];
       
   388 		
       
   389         //Left rounded cap
       
   390         [path appendBezierPathWithArcWithCenter:NSMakePoint(left, bottom + arcRadius)
       
   391 										 radius:arcRadius
       
   392 									 startAngle:270
       
   393 									   endAngle:180
       
   394 									  clockwise:YES];
       
   395         [path lineToPoint: NSMakePoint(left - arcRadius, top - arcRadius)];
       
   396         [path appendBezierPathWithArcWithCenter:NSMakePoint(left, top - arcRadius) radius:arcRadius startAngle:180 endAngle:90 clockwise:YES];
       
   397 
       
   398         if ([object integerValueForProperty:KEY_UNVIEWED_CONTENT]) { //Unviewed
       
   399 			if (flash) {
       
   400                 backColor = [NSColor whiteColor];
       
   401                 textColor = [NSColor blackColor];
       
   402             } else {
       
   403                 backColor = backUnviewedContentColor;
       
   404                 textColor = unviewedContentColor;
       
   405             }
       
   406         } else if ([object boolValueForProperty:@"signedOn"]) { //Signed on
       
   407             backColor = backSignedOnColor;
       
   408             textColor = signedOnColor;
       
   409 			
       
   410         } else if ([object boolValueForProperty:@"signedOff"]) { //Signed off
       
   411             backColor = backSignedOffColor;
       
   412             textColor = signedOffColor;
       
   413 			
       
   414         }
       
   415 		
       
   416 		if (!backColor) {
       
   417 			backColor = [NSColor whiteColor];
       
   418 		}
       
   419 		if (!textColor) {
       
   420 			textColor = [NSColor blackColor];
       
   421 		}
       
   422 		
       
   423         //Lighten/Darken the back color slightly
       
   424         if ([backColor colorIsDark]) {
       
   425             backColor = [backColor darkenBy:-0.15f];
       
   426             borderColor = [backColor darkenBy:-0.3f];
       
   427         } else {
       
   428             backColor = [backColor darkenBy:0.15f];
       
   429             borderColor = [backColor darkenBy:0.3f];
       
   430         }
       
   431 		
       
   432         //Draw
       
   433         [backColor set];
       
   434         [path fill];
       
   435         [borderColor set];
       
   436         [path stroke];
       
   437 		
       
   438         //Get the object's display name
       
   439         [object.displayName drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)
       
   440                            withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]];
       
   441 		/*        
       
   442 			nameString = [[[NSAttributedString alloc] initWithString:contact.displayName attributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]] autorelease];
       
   443         [nameString drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)];*/
       
   444 		
       
   445         //Move down to the next pill
       
   446 		top -= (iconHeight + 7.0f * dockIconScale);
       
   447 		bottom = top - iconHeight;
       
   448     }
       
   449 	
       
   450     [image unlockFocus];
       
   451     
       
   452     return image;
       
   453 }
       
   454 
       
   455 @end