Keep the EKEzvOutgoingFileTransfer.m around so long as it is sending, regardless...
[adiumx.git] / Source / NEHGrowlPlugin.m
blobd7adf2f22d3ecadf69cf115ec3cc431ee552512d
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                 AIContentObject *contentObject = [userInfo objectForKey:@"AIContentObject"];
183                 AIListObject    *source = [contentObject source];
184                 chat = [userInfo objectForKey:@"AIChat"];
186                 if (source) listObject = source;
187         }
189         [clickContext setObject:eventID
190                                          forKey:@"eventID"];
192         if (listObject) {
193                 if ([listObject isKindOfClass:[AIListContact class]]) {
194                         //Use the parent
195                         listObject = [(AIListContact *)listObject parentContact];
196                         title = [listObject longDisplayName];
197                 } else {
198                         title = [listObject displayName];
199                 }
200                 
201                 iconData = [listObject userIconData];
202                 
203                 if (!iconData) {
204                         iconData = [[AIServiceIcons serviceIconForObject:listObject
205                                                                                                                 type:AIServiceIconLarge
206                                                                                                    direction:AIIconNormal] TIFFRepresentation];
207                 }
208                 
209                 if (chat) {
210                         [clickContext setObject:[chat uniqueChatID]
211                                                          forKey:KEY_CHAT_ID];
212                         
213                 } else {
214                         if ([userInfo isKindOfClass:[ESFileTransfer class]] &&
215                                 [eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
216                                 [clickContext setObject:[(ESFileTransfer *)userInfo uniqueID]
217                                                                  forKey:KEY_FILE_TRANSFER_ID];
219                         } else {
220                                 [clickContext setObject:[listObject internalObjectID]
221                                                                  forKey:KEY_LIST_OBJECT_ID];
222                         }
223                 }
225         } else {
226                 if (chat) {
227                         title = [chat name];
229                         [clickContext setObject:[chat uniqueChatID]
230                                                          forKey:KEY_CHAT_ID];
232                         //If we have no listObject or we have a name, we are a group chat and
233                         //should use the account's service icon
234                         iconData = [[AIServiceIcons serviceIconForObject:[chat account]
235                                                                                                                 type:AIServiceIconLarge
236                                                                                                    direction:AIIconNormal] TIFFRepresentation];
237                         
238                 } else {
239                         title = @"Adium";
240                 }
241         }
242         
243         description = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
244                                                                                                                                                          listObject:listObject
245                                                                                                                                                            userInfo:userInfo
246                                                                                                                                                  includeSubject:NO];
248         if (([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES] ||
249            [eventID isEqualToString:CONTACT_STATUS_ONLINE_NO] ||
250            [eventID isEqualToString:CONTACT_STATUS_AWAY_YES] ||
251            [eventID isEqualToString:CONTACT_SEEN_ONLINE_YES] ||
252            [eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]) && 
253                 [(AIListContact *)listObject contactListStatusMessage]) {
254                 description = [description stringByAppendingString:
255                         [@": " stringByAppendingString:
256                                 [[(AIListContact *)listObject contactListStatusMessage] string]]];
257         }
258         
259         if (listObject && [[adium contactAlertsController] isContactStatusEvent:eventID]) {
260                 identifier = [listObject internalObjectID];
261         }
263         NSAssert5((title || description),
264                           @"Growl notify error: EventID %@, listObject %@, userInfo %@\nGave Title \"%@\" description \"%@\"",
265                           eventID,
266                           listObject,
267                           userInfo,
268                           title,
269                           description);
270         
271         [GrowlApplicationBridge notifyWithTitle:title
272                                                                 description:description
273                                                    notificationName:eventID
274                                                                    iconData:iconData
275                                                                    priority:0
276                                                                    isSticky:[[details objectForKey:KEY_GROWL_ALERT_STICKY] boolValue]
277                                                            clickContext:clickContext
278                                                                  identifier:identifier];
280         return YES;
284  * @brief Returns our details pane, an instance of <tt>CBGrowlAlertDetailPane</tt>
285  */
286 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
288     return [CBGrowlAlertDetailPane actionDetailsPane];
292  * @brief Allow multiple actions?
294  * This action should not be performed multiple times for the same triggering event.
295  */
296 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
298         return NO;
301 #pragma mark Growl
304  * @brief Returns the application name Growl will use
305  */
306 - (NSString *)applicationNameForGrowl
308         return @"Adium";
312  * @brief Registration information for Growl
314  * Returns information that Growl needs, like which notifications we will post and our application name.
315  */
316 - (NSDictionary *)registrationDictionaryForGrowl
318         id <AIContactAlertsController> contactAlertsController = [adium contactAlertsController];
319         NSArray                                         *allNotes = [contactAlertsController allEventIDs];
320         NSMutableDictionary                     *humanReadableNames = [NSMutableDictionary dictionary];
321         NSMutableDictionary                     *descriptions = [NSMutableDictionary dictionary];
322         NSEnumerator                            *enumerator;
323         NSString                                        *eventID;
324         
325         enumerator = [allNotes objectEnumerator];
326         while ((eventID = [enumerator nextObject])) {
327                 [humanReadableNames setObject:[contactAlertsController globalShortDescriptionForEventID:eventID]
328                                                            forKey:eventID];
329                 
330                 [descriptions setObject:[contactAlertsController longDescriptionForEventID:eventID 
331                                                                                                                                          forListObject:nil]
332                                                  forKey:eventID];               
333         }
335         NSDictionary    *growlReg = [NSDictionary dictionaryWithObjectsAndKeys:
336                 allNotes, GROWL_NOTIFICATIONS_ALL,
337                 allNotes, GROWL_NOTIFICATIONS_DEFAULT,
338                 humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
339                 descriptions, GROWL_NOTIFICATIONS_DESCRIPTIONS,
340                 nil];
342         return growlReg;
346  * @brief Called when Growl is ready
348  * Currently, this is just used for debugging Growl.
349  */
350 - (void)growlIsReady
352 #ifdef GROWL_DEBUG
353         AILog(@"Growl is go for launch.");
354 #endif
358  * @brief Called when a Growl notification is clicked
360  * When a Growl notificaion is clicked, this method is called, allowing us to take action (e.g. open a new window, make
361  * a conversation active, etc).
363  * @param clickContext A dictionary that was passed to Growl when we installed the notification.
364  */
365 - (void)growlNotificationWasClicked:(NSDictionary *)clickContext
367         NSString                *internalObjectID, *uniqueChatID;
368         AIListObject    *listObject;
369         AIChat                  *chat = nil;
370                 
371         if ((internalObjectID = [clickContext objectForKey:KEY_LIST_OBJECT_ID])) {
372                 if ((listObject = [[adium contactController] existingListObjectWithUniqueID:internalObjectID]) &&
373                         ([listObject isKindOfClass:[AIListContact class]])) {
374                         
375                         //First look for an existing chat to avoid changing anything
376                         if (!(chat = [[adium chatController] existingChatWithContact:(AIListContact *)listObject])) {
377                                 //If we don't find one, create one
378                                 chat = [[adium chatController] openChatWithContact:(AIListContact *)listObject
379                                                                                                 onPreferredAccount:YES];
380                         }
381                 }
383         } else if ((uniqueChatID = [clickContext objectForKey:KEY_CHAT_ID])) {
384                 chat = [[adium chatController] existingChatWithUniqueChatID:uniqueChatID];
385                 
386                 //If we didn't find a chat, it may have closed since the notification was posted.
387                 //If we have an appropriate existing list object, we can create a new chat.
388                 if ((!chat) &&
389                         (listObject = [[adium contactController] existingListObjectWithUniqueID:uniqueChatID]) &&
390                         ([listObject isKindOfClass:[AIListContact class]])) {
391                 
392                         //If the uniqueChatID led us to an existing contact, create a chat with it
393                         chat = [[adium chatController] openChatWithContact:(AIListContact *)listObject
394                                                                                         onPreferredAccount:YES];
395                 }       
396         }
398         NSString *fileTransferID;
399         if ((fileTransferID = [clickContext objectForKey:KEY_FILE_TRANSFER_ID])) {
400                 //If a file transfer notification is clicked, reveal the file
401                 [[ESFileTransfer existingFileTransferWithID:fileTransferID] reveal];
402         }
404         if (chat) {
405                 //Make the chat active
406                 [[adium interfaceController] setActiveChat:chat];
407         }
409         //Make Adium active (needed if, for example, our notification was clicked with another app active)
410         [NSApp activateIgnoringOtherApps:YES];  
414  * @brief The title of the window shown if Growl needs to be installed
415  */
416 - (NSString *)growlInstallationWindowTitle
418         return GROWL_INSTALLATION_WINDOW_TITLE; 
422  * @brief The title of the window shown if Growl needs to be updated
423  */
424 - (NSString *)growlUpdateWindowTitle
426         return GROWL_UPDATE_WINDOW_TITLE;
430  * @brief The body of the window shown if Growl needs to be installed
432  * This method calls _growlInformationForUpdate.
433  */
434 - (NSAttributedString *)growlInstallationInformation
436         return [self _growlInformationForUpdate:NO];
440  * @brief The body of the window shown if Growl needs to be update
442  * This method calls _growlInformationForUpdate.
443  */
444 - (NSAttributedString *)growlUpdateInformation
446         return [self _growlInformationForUpdate:YES];
450  * @brief Returns the body text for the window displayed when Growl needs to be installed or updated
452  * @param isUpdate YES generates the message for the update window, NO likewise for the install window.
453  */
454 - (NSAttributedString *)_growlInformationForUpdate:(BOOL)isUpdate
456         NSMutableAttributedString       *growlInfo;
457         
458         //Start with the window title, centered and bold
459         NSMutableParagraphStyle *centeredStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
460         [centeredStyle setAlignment:NSCenterTextAlignment];
461         
462         growlInfo = [[NSMutableAttributedString alloc] initWithString:(isUpdate ? GROWL_UPDATE_WINDOW_TITLE : GROWL_INSTALLATION_WINDOW_TITLE)
463                                                                                                            attributes:[NSDictionary dictionaryWithObjectsAndKeys:
464                                                                                                                    centeredStyle,NSParagraphStyleAttributeName,
465                                                                                                                    [NSFont boldSystemFontOfSize:GROWL_TEXT_SIZE], NSFontAttributeName,
466                                                                                                                    nil]];
467         //Skip a line
468         [[growlInfo mutableString] appendString:@"\n\n"];
469         
470         //Now provide a default explanation
471         NSAttributedString *defaultExplanation;
472         defaultExplanation = [[[NSAttributedString alloc] initWithString:(isUpdate ? GROWL_UPDATE_EXPLANATION : GROWL_INSTALLATION_EXPLANATION)
473                                                                                                                   attributes:[NSDictionary dictionaryWithObjectsAndKeys:
474                                                                                                                           [NSFont systemFontOfSize:GROWL_TEXT_SIZE], NSFontAttributeName,
475                                                                                                                           nil]] autorelease];
476         
477         [growlInfo appendAttributedString:defaultExplanation];
478         
479         return growlInfo;
482 @end