Now, groups can retrieve a list of the contacts in that group. I probably should...
[adiumx.git] / Source / NEHGrowlPlugin.m
blob4868af98e7128d3759bdf8157de94f44167fb328
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 #import "NEHGrowlPlugin.h"
18 #import "CBGrowlAlertDetailPane.h"
19 #import <Adium/AIChatControllerProtocol.h>
20 #import <Adium/AIContactControllerProtocol.h>
21 #import <Adium/AIContentControllerProtocol.h>
22 #import <Adium/AIInterfaceControllerProtocol.h>
23 #import <Adium/AIContactAlertsControllerProtocol.h>
24 #import <Adium/AIAccount.h>
25 #import <Adium/AIChat.h>
26 #import <Adium/AIContentObject.h>
27 #import <Adium/AIListContact.h>
28 #import <Adium/AIListObject.h>
29 #import <Adium/AIServiceIcons.h>
30 #import <Adium/ESFileTransfer.h>
31 #import <AIUtilities/AIApplicationAdditions.h>
32 #import <AIUtilities/AIImageAdditions.h>
33 #import <Growl-WithInstaller/Growl.h>
35 //#define GROWL_DEBUG 1
37 #define GROWL_ALERT                                                     AILocalizedString(@"Display a Growl notification",nil)
38 #define GROWL_STICKY_ALERT                                      AILocalizedString(@"Display a sticky Growl notification",nil)
40 #define GROWL_INSTALLATION_WINDOW_TITLE         AILocalizedString(@"Growl Installation Recommended", "Growl installation window title")
41 #define GROWL_UPDATE_WINDOW_TITLE                       AILocalizedString(@"Growl Update Available", "Growl update window title")
43 #define GROWL_INSTALLATION_EXPLANATION          AILocalizedString(@"Adium uses the Growl notification system to provide a configurable interface to display status changes, incoming messages and more.\n\nIt is strongly recommended that you allow Adium to automatically install Growl; no download is required.","Growl installation explanation")
44 #define GROWL_UPDATE_EXPLANATION                        AILocalizedString(@"Adium uses the Growl notification system to provide a configurable interface to display status changes, incoming messages and more.\n\nThis release of Adium includes an updated version of Growl. It is strongly recommended that you allow Adium to automatically update Growl; no download is required.","Growl update explanation")
46 #define GROWL_TEXT_SIZE 11
48 #define GROWL_EVENT_ALERT_IDENTIFIER            @"Growl"
50 #define KEY_FILE_TRANSFER_ID    @"fileTransferUniqueID"
51 #define KEY_CHAT_ID                             @"uniqueChatID"
52 #define KEY_LIST_OBJECT_ID              @"internalObjectID"
54 @interface NEHGrowlPlugin (PRIVATE)
55 - (NSDictionary *)growlRegistrationDict;
56 - (NSAttributedString *)_growlInformationForUpdate:(BOOL)isUpdate;
57 @end
59 /*!
60  * @class NEHGrowlPlugin
61  * @brief Implements Growl functionality in Adium
62  *
63  * This class manages the Growl event type, and controls the display of Growl notifications that Adium generates.
64  */
65 @implementation NEHGrowlPlugin
67 /*!
68  * @brief Initialize the Growl plugin
69  *
70  * Waits for Adium to finish launching before we perform further actions so all events are registered.
71  */
72 - (void)installPlugin
74         [[adium notificationCenter] addObserver:self
75                                                                    selector:@selector(adiumFinishedLaunching:)
76                                                                            name:AIApplicationDidFinishLoadingNotification
77                                                                          object:nil];
80 /*!
81  * @brief Deallocate
82  */
83 - (void)dealloc
85         [super dealloc];
88 /*!
89  * @brief Adium finished launching
90  *
91  * Delays one more run loop so any events which are registered on this notification are guaranteed to be complete
92  * regardless of the order in which the observers are called.
93  */
94 - (void)adiumFinishedLaunching:(NSNotification *)notification
96         [self performSelector:@selector(beginGrowling)
97                            withObject:nil
98                            afterDelay:0.00001];
100         [[adium notificationCenter] removeObserver:self
101                                                                                   name:AIApplicationDidFinishLoadingNotification
102                                                                                 object:nil];
106  * @brief Begin accepting Growl events
107  */
108 - (void)beginGrowling
110         [GrowlApplicationBridge setGrowlDelegate:self];
112         //Install our contact alert
113         [[adium contactAlertsController] registerActionID:GROWL_EVENT_ALERT_IDENTIFIER withHandler:self];
114         
115 #ifdef GROWL_DEBUG
116         [GrowlApplicationBridge notifyWithTitle:@"We have found a witch."
117                                                                 description:@"May we burn her?"
118                                                    notificationName:CONTENT_MESSAGE_RECEIVED
119                                                                    iconData:nil
120                                                                    priority:0
121                                                                    isSticky:YES
122                                                            clickContext:[NSDictionary dictionaryWithObjectsAndKeys:
123                                                                    @"AIM.tekjew", @"internalObjectID",
124                                                                    CONTENT_MESSAGE_RECEIVED, @"eventID",
125                                                                    nil]];
126 #endif
129 #pragma mark AIActionHandler
131  * @brief Returns a short description of Growl events
132  */
133 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
135         return GROWL_ALERT;
139  * @brief Returns a long description of Growl events
141  * The long description reflects the "sticky"-ness of the notification.
142  */
143 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
145         if ([[details objectForKey:KEY_GROWL_ALERT_STICKY] boolValue]) {
146                 return GROWL_STICKY_ALERT;
147         } else {
148                 return GROWL_ALERT;
149         }
153  * @brief Returns the image associated with the Growl event
154  */
155 - (NSImage *)imageForActionID:(NSString *)actionID
157         return [NSImage imageNamed:@"GrowlAlert" forClass:[self class]];
161  * @brief Post a notification for Growl for display
163  * This method is called when by Adium when a Growl alert is activated. It passes this information on to Growl, which displays a notificaion.
165  * @param actionID The Action ID being performed, in this case the Growl plugin's Action ID.
166  * @param listObject The list object the event is related to
167  * @param details A dictionary containing additional information about the event
168  * @param eventID The ID of the event (e.g. new message, contact went away, etc)
169  * @param userInfo Any additional information
170  */
171 - (BOOL)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
173         NSString                        *title, *description;
174         AIChat                          *chat = nil;
175         NSData                          *iconData = nil;
176         NSMutableDictionary     *clickContext = [NSMutableDictionary dictionary];
177         NSString                        *identifier = nil;
179         //For a message event, listObject should become whoever sent the message
180         if ([[adium contactAlertsController] isMessageEvent:eventID] &&
181                 [userInfo respondsToSelector:@selector(objectForKey:)] &&
182                 [userInfo objectForKey:@"AIContentObject"]) {
183                 AIContentObject *contentObject = [userInfo objectForKey:@"AIContentObject"];
184                 AIListObject    *source = [contentObject source];
185                 chat = [userInfo objectForKey:@"AIChat"];
187                 if (source) listObject = source;
188         }
190         [clickContext setObject:eventID
191                                          forKey:@"eventID"];
193         if (listObject) {
194                 if ([listObject isKindOfClass:[AIListContact class]]) {
195                         //Use the parent
196                         listObject = [(AIListContact *)listObject parentContact];
197                         title = [listObject longDisplayName];
198                 } else {
199                         title = [listObject displayName];
200                 }
201                 
202                 iconData = [listObject userIconData];
203                 
204                 if (!iconData) {
205                         iconData = [[AIServiceIcons serviceIconForObject:listObject
206                                                                                                                 type:AIServiceIconLarge
207                                                                                                    direction:AIIconNormal] TIFFRepresentation];
208                 }
209                 
210                 if (chat) {
211                         [clickContext setObject:[chat uniqueChatID]
212                                                          forKey:KEY_CHAT_ID];
213                         
214                 } else {
215                         if ([userInfo isKindOfClass:[ESFileTransfer class]] &&
216                                 [eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
217                                 [clickContext setObject:[(ESFileTransfer *)userInfo uniqueID]
218                                                                  forKey:KEY_FILE_TRANSFER_ID];
220                         } else {
221                                 [clickContext setObject:[listObject internalObjectID]
222                                                                  forKey:KEY_LIST_OBJECT_ID];
223                         }
224                 }
226         } else {
227                 if (chat) {
228                         title = [chat name];
230                         [clickContext setObject:[chat uniqueChatID]
231                                                          forKey:KEY_CHAT_ID];
233                         //If we have no listObject or we have a name, we are a group chat and
234                         //should use the account's service icon
235                         iconData = [[AIServiceIcons serviceIconForObject:[chat account]
236                                                                                                                 type:AIServiceIconLarge
237                                                                                                    direction:AIIconNormal] TIFFRepresentation];
238                         
239                 } else {
240                         title = @"Adium";
241                 }
242         }
243         
244         description = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
245                                                                                                                                                          listObject:listObject
246                                                                                                                                                            userInfo:userInfo
247                                                                                                                                                  includeSubject:NO];
249         if (([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES] ||
250            [eventID isEqualToString:CONTACT_STATUS_ONLINE_NO] ||
251            [eventID isEqualToString:CONTACT_STATUS_AWAY_YES] ||
252            [eventID isEqualToString:CONTACT_SEEN_ONLINE_YES] ||
253            [eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]) && 
254                 [(AIListContact *)listObject contactListStatusMessage]) {
255                 description = [description stringByAppendingString:
256                         [@": " stringByAppendingString:
257                                 [[(AIListContact *)listObject contactListStatusMessage] string]]];
258         }
259         
260         if (listObject && [[adium contactAlertsController] isContactStatusEvent:eventID]) {
261                 identifier = [listObject internalObjectID];
262         }
264         NSAssert5((title || description),
265                           @"Growl notify error: EventID %@, listObject %@, userInfo %@\nGave Title \"%@\" description \"%@\"",
266                           eventID,
267                           listObject,
268                           userInfo,
269                           title,
270                           description);
271         
272         [GrowlApplicationBridge notifyWithTitle:title
273                                                                 description:description
274                                                    notificationName:eventID
275                                                                    iconData:iconData
276                                                                    priority:0
277                                                                    isSticky:[[details objectForKey:KEY_GROWL_ALERT_STICKY] boolValue]
278                                                            clickContext:clickContext
279                                                                  identifier:identifier];
281         return YES;
285  * @brief Returns our details pane, an instance of <tt>CBGrowlAlertDetailPane</tt>
286  */
287 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
289     return [CBGrowlAlertDetailPane actionDetailsPane];
293  * @brief Allow multiple actions?
295  * This action should not be performed multiple times for the same triggering event.
296  */
297 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
299         return NO;
302 #pragma mark Growl
305  * @brief Returns the application name Growl will use
306  */
307 - (NSString *)applicationNameForGrowl
309         return @"Adium";
313  * @brief Registration information for Growl
315  * Returns information that Growl needs, like which notifications we will post and our application name.
316  */
317 - (NSDictionary *)registrationDictionaryForGrowl
319         id <AIContactAlertsController> contactAlertsController = [adium contactAlertsController];
320         NSArray                                         *allNotes = [contactAlertsController allEventIDs];
321         NSMutableDictionary                     *humanReadableNames = [NSMutableDictionary dictionary];
322         NSMutableDictionary                     *descriptions = [NSMutableDictionary dictionary];
323         NSEnumerator                            *enumerator;
324         NSString                                        *eventID;
325         
326         enumerator = [allNotes objectEnumerator];
327         while ((eventID = [enumerator nextObject])) {
328                 [humanReadableNames setObject:[contactAlertsController globalShortDescriptionForEventID:eventID]
329                                                            forKey:eventID];
330                 
331                 [descriptions setObject:[contactAlertsController longDescriptionForEventID:eventID 
332                                                                                                                                          forListObject:nil]
333                                                  forKey:eventID];               
334         }
336         NSDictionary    *growlReg = [NSDictionary dictionaryWithObjectsAndKeys:
337                 allNotes, GROWL_NOTIFICATIONS_ALL,
338                 allNotes, GROWL_NOTIFICATIONS_DEFAULT,
339                 humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
340                 descriptions, GROWL_NOTIFICATIONS_DESCRIPTIONS,
341                 nil];
343         return growlReg;
347  * @brief Called when Growl is ready
349  * Currently, this is just used for debugging Growl.
350  */
351 - (void)growlIsReady
353 #ifdef GROWL_DEBUG
354         AILog(@"Growl is go for launch.");
355 #endif
359  * @brief Called when a Growl notification is clicked
361  * When a Growl notificaion is clicked, this method is called, allowing us to take action (e.g. open a new window, make
362  * a conversation active, etc).
364  * @param clickContext A dictionary that was passed to Growl when we installed the notification.
365  */
366 - (void)growlNotificationWasClicked:(NSDictionary *)clickContext
368         NSString                *internalObjectID, *uniqueChatID;
369         AIListObject    *listObject;
370         AIChat                  *chat = nil;
371                 
372         if ((internalObjectID = [clickContext objectForKey:KEY_LIST_OBJECT_ID])) {
373                 if ((listObject = [[adium contactController] existingListObjectWithUniqueID:internalObjectID]) &&
374                         ([listObject isKindOfClass:[AIListContact class]])) {
375                         
376                         //First look for an existing chat to avoid changing anything
377                         if (!(chat = [[adium chatController] existingChatWithContact:(AIListContact *)listObject])) {
378                                 //If we don't find one, create one
379                                 chat = [[adium chatController] openChatWithContact:(AIListContact *)listObject
380                                                                                                 onPreferredAccount:YES];
381                         }
382                 }
384         } else if ((uniqueChatID = [clickContext objectForKey:KEY_CHAT_ID])) {
385                 chat = [[adium chatController] existingChatWithUniqueChatID:uniqueChatID];
386                 
387                 //If we didn't find a chat, it may have closed since the notification was posted.
388                 //If we have an appropriate existing list object, we can create a new chat.
389                 if ((!chat) &&
390                         (listObject = [[adium contactController] existingListObjectWithUniqueID:uniqueChatID]) &&
391                         ([listObject isKindOfClass:[AIListContact class]])) {
392                 
393                         //If the uniqueChatID led us to an existing contact, create a chat with it
394                         chat = [[adium chatController] openChatWithContact:(AIListContact *)listObject
395                                                                                         onPreferredAccount:YES];
396                 }       
397         }
399         NSString *fileTransferID;
400         if ((fileTransferID = [clickContext objectForKey:KEY_FILE_TRANSFER_ID])) {
401                 //If a file transfer notification is clicked, reveal the file
402                 [[ESFileTransfer existingFileTransferWithID:fileTransferID] reveal];
403         }
405         if (chat) {
406                 //Make the chat active
407                 [[adium interfaceController] setActiveChat:chat];
408         }
410         //Make Adium active (needed if, for example, our notification was clicked with another app active)
411         [NSApp activateIgnoringOtherApps:YES];  
415  * @brief The title of the window shown if Growl needs to be installed
416  */
417 - (NSString *)growlInstallationWindowTitle
419         return GROWL_INSTALLATION_WINDOW_TITLE; 
423  * @brief The title of the window shown if Growl needs to be updated
424  */
425 - (NSString *)growlUpdateWindowTitle
427         return GROWL_UPDATE_WINDOW_TITLE;
431  * @brief The body of the window shown if Growl needs to be installed
433  * This method calls _growlInformationForUpdate.
434  */
435 - (NSAttributedString *)growlInstallationInformation
437         return [self _growlInformationForUpdate:NO];
441  * @brief The body of the window shown if Growl needs to be update
443  * This method calls _growlInformationForUpdate.
444  */
445 - (NSAttributedString *)growlUpdateInformation
447         return [self _growlInformationForUpdate:YES];
451  * @brief Returns the body text for the window displayed when Growl needs to be installed or updated
453  * @param isUpdate YES generates the message for the update window, NO likewise for the install window.
454  */
455 - (NSAttributedString *)_growlInformationForUpdate:(BOOL)isUpdate
457         NSMutableAttributedString       *growlInfo;
458         
459         //Start with the window title, centered and bold
460         NSMutableParagraphStyle *centeredStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
461         [centeredStyle setAlignment:NSCenterTextAlignment];
462         
463         growlInfo = [[NSMutableAttributedString alloc] initWithString:(isUpdate ? GROWL_UPDATE_WINDOW_TITLE : GROWL_INSTALLATION_WINDOW_TITLE)
464                                                                                                            attributes:[NSDictionary dictionaryWithObjectsAndKeys:
465                                                                                                                    centeredStyle,NSParagraphStyleAttributeName,
466                                                                                                                    [NSFont boldSystemFontOfSize:GROWL_TEXT_SIZE], NSFontAttributeName,
467                                                                                                                    nil]];
468         //Skip a line
469         [[growlInfo mutableString] appendString:@"\n\n"];
470         
471         //Now provide a default explanation
472         NSAttributedString *defaultExplanation;
473         defaultExplanation = [[[NSAttributedString alloc] initWithString:(isUpdate ? GROWL_UPDATE_EXPLANATION : GROWL_INSTALLATION_EXPLANATION)
474                                                                                                                   attributes:[NSDictionary dictionaryWithObjectsAndKeys:
475                                                                                                                           [NSFont systemFontOfSize:GROWL_TEXT_SIZE], NSFontAttributeName,
476                                                                                                                           nil]] autorelease];
477         
478         [growlInfo appendAttributedString:defaultExplanation];
479         
480         return growlInfo;
483 @end