2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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;
60 * @class NEHGrowlPlugin
61 * @brief Implements Growl functionality in Adium
63 * This class manages the Growl event type, and controls the display of Growl notifications that Adium generates.
65 @implementation NEHGrowlPlugin
68 * @brief Initialize the Growl plugin
70 * Waits for Adium to finish launching before we perform further actions so all events are registered.
74 [[adium notificationCenter] addObserver:self
75 selector:@selector(adiumFinishedLaunching:)
76 name:AIApplicationDidFinishLoadingNotification
89 * @brief Adium finished launching
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.
94 - (void)adiumFinishedLaunching:(NSNotification *)notification
96 [self performSelector:@selector(beginGrowling)
100 [[adium notificationCenter] removeObserver:self
101 name:AIApplicationDidFinishLoadingNotification
106 * @brief Begin accepting Growl events
108 - (void)beginGrowling
110 [GrowlApplicationBridge setGrowlDelegate:self];
112 //Install our contact alert
113 [[adium contactAlertsController] registerActionID:GROWL_EVENT_ALERT_IDENTIFIER withHandler:self];
116 [GrowlApplicationBridge notifyWithTitle:@"We have found a witch."
117 description:@"May we burn her?"
118 notificationName:CONTENT_MESSAGE_RECEIVED
122 clickContext:[NSDictionary dictionaryWithObjectsAndKeys:
123 @"AIM.tekjew", @"internalObjectID",
124 CONTENT_MESSAGE_RECEIVED, @"eventID",
129 #pragma mark AIActionHandler
131 * @brief Returns a short description of Growl events
133 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
139 * @brief Returns a long description of Growl events
141 * The long description reflects the "sticky"-ness of the notification.
143 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
145 if ([[details objectForKey:KEY_GROWL_ALERT_STICKY] boolValue]) {
146 return GROWL_STICKY_ALERT;
153 * @brief Returns the image associated with the Growl event
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
171 - (BOOL)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
173 NSString *title, *description;
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;
189 [clickContext setObject:eventID
193 if ([listObject isKindOfClass:[AIListContact class]]) {
195 listObject = [(AIListContact *)listObject parentContact];
196 title = [listObject longDisplayName];
198 title = [listObject displayName];
201 iconData = [listObject userIconData];
204 iconData = [[AIServiceIcons serviceIconForObject:listObject
205 type:AIServiceIconLarge
206 direction:AIIconNormal] TIFFRepresentation];
210 [clickContext setObject:[chat uniqueChatID]
214 if ([userInfo isKindOfClass:[ESFileTransfer class]] &&
215 [eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
216 [clickContext setObject:[(ESFileTransfer *)userInfo uniqueID]
217 forKey:KEY_FILE_TRANSFER_ID];
220 [clickContext setObject:[listObject internalObjectID]
221 forKey:KEY_LIST_OBJECT_ID];
229 [clickContext setObject:[chat uniqueChatID]
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];
243 description = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
244 listObject:listObject
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]]];
259 if (listObject && [[adium contactAlertsController] isContactStatusEvent:eventID]) {
260 identifier = [listObject internalObjectID];
263 NSAssert5((title || description),
264 @"Growl notify error: EventID %@, listObject %@, userInfo %@\nGave Title \"%@\" description \"%@\"",
271 [GrowlApplicationBridge notifyWithTitle:title
272 description:description
273 notificationName:eventID
276 isSticky:[[details objectForKey:KEY_GROWL_ALERT_STICKY] boolValue]
277 clickContext:clickContext
278 identifier:identifier];
284 * @brief Returns our details pane, an instance of <tt>CBGrowlAlertDetailPane</tt>
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.
296 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
304 * @brief Returns the application name Growl will use
306 - (NSString *)applicationNameForGrowl
312 * @brief Registration information for Growl
314 * Returns information that Growl needs, like which notifications we will post and our application name.
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;
325 enumerator = [allNotes objectEnumerator];
326 while ((eventID = [enumerator nextObject])) {
327 [humanReadableNames setObject:[contactAlertsController globalShortDescriptionForEventID:eventID]
330 [descriptions setObject:[contactAlertsController longDescriptionForEventID:eventID
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,
346 * @brief Called when Growl is ready
348 * Currently, this is just used for debugging Growl.
353 AILog(@"Growl is go for launch.");
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.
365 - (void)growlNotificationWasClicked:(NSDictionary *)clickContext
367 NSString *internalObjectID, *uniqueChatID;
368 AIListObject *listObject;
371 if ((internalObjectID = [clickContext objectForKey:KEY_LIST_OBJECT_ID])) {
372 if ((listObject = [[adium contactController] existingListObjectWithUniqueID:internalObjectID]) &&
373 ([listObject isKindOfClass:[AIListContact class]])) {
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];
383 } else if ((uniqueChatID = [clickContext objectForKey:KEY_CHAT_ID])) {
384 chat = [[adium chatController] existingChatWithUniqueChatID:uniqueChatID];
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.
389 (listObject = [[adium contactController] existingListObjectWithUniqueID:uniqueChatID]) &&
390 ([listObject isKindOfClass:[AIListContact class]])) {
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];
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];
405 //Make the chat active
406 [[adium interfaceController] setActiveChat:chat];
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
416 - (NSString *)growlInstallationWindowTitle
418 return GROWL_INSTALLATION_WINDOW_TITLE;
422 * @brief The title of the window shown if Growl needs to be updated
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.
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.
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.
454 - (NSAttributedString *)_growlInformationForUpdate:(BOOL)isUpdate
456 NSMutableAttributedString *growlInfo;
458 //Start with the window title, centered and bold
459 NSMutableParagraphStyle *centeredStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
460 [centeredStyle setAlignment:NSCenterTextAlignment];
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,
468 [[growlInfo mutableString] appendString:@"\n\n"];
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,
477 [growlInfo appendAttributedString:defaultExplanation];