Plugins/Dock Icon Badging/AIDockNameOverlay.m
changeset 3670 add0c83648a5
child 3688 7e0b0cdfc8ff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Dock Icon Badging/AIDockNameOverlay.m	Fri Feb 18 15:44:06 2011 -0500
@@ -0,0 +1,342 @@
+#import "AIDockNameOverlay.h"
+#import "AIDockController.h"
+#import <Adium/AIChatControllerProtocol.h>
+#import <Adium/AIContactAlertsControllerProtocol.h>
+#import <Adium/AIStatusControllerProtocol.h>
+#import <AIUtilities/AIParagraphStyleAdditions.h>
+#import <AIUtilities/AIImageAdditions.h>
+#import <Adium/AIChat.h>
+#import <Adium/AIAccount.h>
+#import <AIUtilities/AIArrayAdditions.h>
+#import <Adium/AIAbstractListController.h>
+#import <AIUtilities/AIColorAdditions.h>
+
+
+#define DOCK_OVERLAY_ALERT_SHORT	AILocalizedString(@"Display name in the dock icon",nil)
+#define DOCK_OVERLAY_ALERT_LONG		DOCK_OVERLAY_ALERT_SHORT
+
+@interface AIDockNameOverlay ()
+- (void)flushPreferenceColorCache;
+- (void)drawOverlay;
+- (void)removeDockOverlay:(NSTimer *)removeTimer;
+@end
+
+@implementation AIDockNameOverlay
+- (void)installPlugin
+{
+	//Install our contact alert
+	[adium.contactAlertsController registerActionID:DOCK_OVERLAY_ALERT_IDENTIFIER withHandler:self];
+
+	overlayObjectsArray = [[NSMutableArray alloc] init];
+	
+	//Register as a contact observer (For signed on / signed off)
+	[[AIContactObserverManager sharedManager] registerListObjectObserver:self];
+	
+	[adium.chatController registerChatObserver:self];
+	
+	//Observe pref changes
+	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
+	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
+}
+
+- (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
+							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
+{	
+	if ([group isEqualToString:PREF_GROUP_LIST_THEME]) {
+		//Grab colors from status coloring plugin's prefs
+		[self flushPreferenceColorCache];
+		signedOffColor = [[[prefDict objectForKey:KEY_SIGNED_OFF_COLOR] representedColor] retain];
+		signedOnColor = [[[prefDict objectForKey:KEY_SIGNED_ON_COLOR] representedColor] retain];
+		unviewedContentColor = [[[prefDict objectForKey:KEY_UNVIEWED_COLOR] representedColor] retain];
+		
+		backSignedOffColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_OFF_COLOR] representedColor] retain];
+		backSignedOnColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_ON_COLOR] representedColor] retain];
+		backUnviewedContentColor = [[[prefDict objectForKey:KEY_LABEL_UNVIEWED_COLOR] representedColor] retain];
+	}
+}
+
+- (void)flushPreferenceColorCache
+{
+	[signedOffColor release]; signedOffColor = nil;
+	[signedOnColor release]; signedOnColor = nil;
+	[unviewedContentColor release]; unviewedContentColor = nil;
+	[backSignedOffColor release]; backSignedOffColor = nil;
+	[backSignedOnColor release]; backSignedOnColor = nil;
+	[backUnviewedContentColor release]; backUnviewedContentColor = nil;
+}
+
+- (void)uninstallPlugin
+{
+	[adium.preferenceController unregisterPreferenceObserver:self];
+	[adium.chatController unregisterChatObserver:self];
+}
+
+/*!
+ * @brief Short description
+ * @result A short localized description of the action
+ */
+- (NSString *)shortDescriptionForActionID:(NSString *)actionID
+{
+	return DOCK_OVERLAY_ALERT_SHORT;
+}
+
+/*!
+ * @brief Long description
+ * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
+ */
+- (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
+{
+	return DOCK_OVERLAY_ALERT_LONG;
+}
+
+/*!
+ * @brief Image
+ */
+- (NSImage *)imageForActionID:(NSString *)actionID
+{
+	return [NSImage imageNamed:@"DockAlert" forClass:[self class]];
+}
+
+/*!
+ * @brief Details pane
+ * @result An <tt>AIModularPane</tt> to use for configuring this action, or nil if no configuration is possible.
+ */
+- (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
+{
+	return nil;
+}
+
+/*!
+ * @brief Perform an action
+ *
+ * @param actionID The ID of the action to perform
+ * @param listObject The listObject associated with the event triggering the action. It may be nil
+ * @param details If set by the details pane when the action was created, the details dictionary for this particular action
+ * @param eventID The eventID which triggered this action
+ * @param userInfo Additional information associated with the event; userInfo's type will vary with the actionID.
+ */
+- (BOOL)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
+{
+	BOOL isMessageEvent = [adium.contactAlertsController isMessageEvent:eventID];
+	
+	if (isMessageEvent) {
+		AIChat *chat;
+		
+		if ((chat = [userInfo objectForKey:@"AIChat"]) &&
+			(chat != adium.interfaceController.activeChat)) {
+			if (![overlayObjectsArray containsObjectIdenticalTo:chat])
+				[overlayObjectsArray addObject:chat];
+			
+			//Wait until the next run loop so that this event is done processing and our unviewed content count is correct
+			[self performSelector:@selector(drawOverlay)
+					   withObject:nil
+					   afterDelay:0];
+		}
+		
+	} else if (listObject) {
+		NSTimer *removeTimer;
+		
+		//Clear any current timer for this object to have its overlay removed
+		if ((removeTimer = [listObject valueForProperty:@"DockOverlayRemoveTimer"])) [removeTimer invalidate];
+		
+		//Add a timer to remove this overlay
+		removeTimer = [NSTimer scheduledTimerWithTimeInterval:5
+													   target:self
+													 selector:@selector(removeDockOverlay:)
+													 userInfo:listObject
+													  repeats:NO];
+		[listObject setValue:removeTimer
+				 forProperty:@"DockOverlayRemoveTimer"
+					  notify:NotifyNever];
+		
+		if (![overlayObjectsArray containsObjectIdenticalTo:listObject])
+			[overlayObjectsArray addObject:listObject];
+		
+		[self drawOverlay];
+	}
+	
+	return YES;
+}
+
+/*!
+ * @brief Allow multiple actions?
+ *
+ * If this method returns YES, every one of this action associated with the triggering event will be executed.
+ * If this method returns NO, only the first will be.
+ */
+- (BOOL)allowMultipleActionsWithID:(NSString *)actionID
+{
+	return NO;
+}
+
+/*!
+ * @brief When a chat no longer has unviewed content remove it from display
+ */
+- (NSSet *)updateChat:(AIChat *)inChat keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
+{
+	if (inModifiedKeys == nil || [inModifiedKeys containsObject:KEY_UNVIEWED_CONTENT]) {
+		if (![inChat unviewedContentCount]) {
+			[overlayObjectsArray removeObjectIdenticalTo:inChat];
+			[self drawOverlay];
+		}
+	}
+	
+	return nil;
+}
+
+/*!
+ * @brief When an account signs on or off force an overlay update as its contacts statuses may have silently changed
+ */
+- (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
+{
+	if ([inObject isKindOfClass:[AIAccount class]]) {
+		if ([inModifiedKeys containsObject:@"isOnline"]) {
+			BOOL madeChanges = NO;
+			
+			for (AIListObject *listObject in [[overlayObjectsArray copy] autorelease]) {
+				if (([listObject respondsToSelector:@selector(account)]) &&
+					([(id)listObject account] == inObject) &&
+					([overlayObjectsArray containsObjectIdenticalTo:listObject])) {
+					[overlayObjectsArray removeObjectIdenticalTo:listObject];
+					madeChanges = YES;
+				}
+			}
+			
+			if (madeChanges) [self drawOverlay];
+		}
+	}
+	
+	return nil;
+}
+
+- (void)removeDockOverlay:(NSTimer *)removeTimer
+{
+	AIListObject *inObject = [removeTimer userInfo];
+	[overlayObjectsArray removeObjectIdenticalTo:inObject];
+	
+	[inObject setValue:nil
+		   forProperty:@"DockOverlayRemoveTimer"
+				notify:NotifyNever];
+	
+	[self drawOverlay];
+}
+
+- (void)drawOverlay
+{
+	NSFont				*font;
+	NSParagraphStyle	*paragraphStyle;
+	CGFloat				iconHeight;
+	CGFloat				top, bottom;
+	NSImage				*image = [[NSImage alloc] initWithSize:NSMakeSize(128, 128)];
+	
+	iconHeight = 30.0f;
+	bottom = 2;
+	top = bottom + iconHeight;
+	
+	//Set up the string details
+	font = [NSFont boldSystemFontOfSize:24.0f];
+	paragraphStyle = [NSParagraphStyle styleWithAlignment:NSCenterTextAlignment lineBreakMode:NSLineBreakByClipping];
+	
+	[image lockFocus];
+	
+	//Clear our image
+	[[NSColor clearColor] set];
+	NSRectFillUsingOperation(NSMakeRect(0, 0, 128, 128), NSCompositeCopy);
+	
+	//Draw overlays for each contact
+	for (ESObjectWithProperties *object in [overlayObjectsArray reverseObjectEnumerator]) {
+		if (top >= 128)
+			break;
+		
+		CGFloat			left, right, arcRadius, stringInset;
+		NSBezierPath	*path;
+		NSColor			*backColor = nil, *textColor = nil, *borderColor = nil;
+		
+		//Create the pill frame
+		arcRadius = (iconHeight / 2.0f);
+		stringInset = (iconHeight / 4.0f);
+		left = 1 + arcRadius;
+		right = 127 - arcRadius;
+		
+		path = [NSBezierPath bezierPath];
+		[path setLineWidth:((iconHeight/2.0f) * 0.13333f)];
+		//Top
+		[path moveToPoint: NSMakePoint(left, top)];
+		[path lineToPoint: NSMakePoint(right, top)];
+		
+		//Right rounded cap
+		[path appendBezierPathWithArcWithCenter:NSMakePoint(right, top - arcRadius)
+										 radius:arcRadius
+									 startAngle:90
+									   endAngle:0
+									  clockwise:YES];
+		[path lineToPoint: NSMakePoint(right + arcRadius, bottom + arcRadius)];
+		[path appendBezierPathWithArcWithCenter:NSMakePoint(right, bottom + arcRadius)
+										 radius:arcRadius
+									 startAngle:0
+									   endAngle:270
+									  clockwise:YES];
+		
+		//Bottom
+		[path moveToPoint: NSMakePoint(right, bottom)];
+		[path lineToPoint: NSMakePoint(left, bottom)];
+		
+		//Left rounded cap
+		[path appendBezierPathWithArcWithCenter:NSMakePoint(left, bottom + arcRadius)
+										 radius:arcRadius
+									 startAngle:270
+									   endAngle:180
+									  clockwise:YES];
+		[path lineToPoint: NSMakePoint(left - arcRadius, top - arcRadius)];
+		[path appendBezierPathWithArcWithCenter:NSMakePoint(left, top - arcRadius)
+										 radius:arcRadius
+									 startAngle:180
+									   endAngle:90
+									  clockwise:YES];
+		
+		if ([object integerValueForProperty:KEY_UNVIEWED_CONTENT]) { //Unviewed
+			backColor = backUnviewedContentColor;
+			textColor = unviewedContentColor;
+		} else if ([object boolValueForProperty:@"signedOn"]) { //Signed on
+			backColor = backSignedOnColor;
+			textColor = signedOnColor;
+		} else if ([object boolValueForProperty:@"signedOff"]) { //Signed off
+			backColor = backSignedOffColor;
+			textColor = signedOffColor;
+		}
+		
+		if (!backColor)
+			backColor = [NSColor whiteColor];
+		if (!textColor)
+			textColor = [NSColor blackColor];
+		
+		//Lighten/Darken the back color slightly
+		if ([backColor colorIsDark]) {
+			backColor = [backColor darkenBy:-0.15f];
+			borderColor = [backColor darkenBy:-0.3f];
+		} else {
+			backColor = [backColor darkenBy:0.15f];
+			borderColor = [backColor darkenBy:0.3f];
+		}
+		
+		//Draw
+		[backColor set];
+		[path fill];
+		[borderColor set];
+		[path stroke];
+		
+		//Get the object's display name
+		[object.displayName drawInRect:NSMakeRect(stringInset + 1, bottom - 1, 127 - (stringInset * 2), top - bottom)
+						withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]];
+		
+		//Move up to the next pill
+		bottom = top + 3.0f;
+		top = bottom + iconHeight;
+	}
+	
+	[image unlockFocus];
+	
+	[adium.dockController setOverlay:image];
+}
+
+@end