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 [userInfo objectForKey:@"AIContentObject"]) {
183 AIContentObject *contentObject = [userInfo objectForKey:@"AIContentObject"];
184 AIListObject *source = [contentObject source];
185 chat = [userInfo objectForKey:@"AIChat"];
187 if (source) listObject = source;
190 [clickContext setObject:eventID
194 if ([listObject isKindOfClass:[AIListContact class]]) {
196 listObject = [(AIListContact *)listObject parentContact];
197 title = [listObject longDisplayName];
199 title = [listObject displayName];
202 iconData = [listObject userIconData];
205 iconData = [[AIServiceIcons serviceIconForObject:listObject
206 type:AIServiceIconLarge
207 direction:AIIconNormal] TIFFRepresentation];
211 [clickContext setObject:[chat uniqueChatID]
215 if ([userInfo isKindOfClass:[ESFileTransfer class]] &&
216 [eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
217 [clickContext setObject:[(ESFileTransfer *)userInfo uniqueID]
218 forKey:KEY_FILE_TRANSFER_ID];
221 [clickContext setObject:[listObject internalObjectID]
222 forKey:KEY_LIST_OBJECT_ID];
230 [clickContext setObject:[chat uniqueChatID]
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];
244 description = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
245 listObject:listObject
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]]];
260 if (listObject && [[adium contactAlertsController] isContactStatusEvent:eventID]) {
261 identifier = [listObject internalObjectID];
264 NSAssert5((title || description),
265 @"Growl notify error: EventID %@, listObject %@, userInfo %@\nGave Title \"%@\" description \"%@\"",
272 [GrowlApplicationBridge notifyWithTitle:title
273 description:description
274 notificationName:eventID
277 isSticky:[[details objectForKey:KEY_GROWL_ALERT_STICKY] boolValue]
278 clickContext:clickContext
279 identifier:identifier];
285 * @brief Returns our details pane, an instance of <tt>CBGrowlAlertDetailPane</tt>
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.
297 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
305 * @brief Returns the application name Growl will use
307 - (NSString *)applicationNameForGrowl
313 * @brief Registration information for Growl
315 * Returns information that Growl needs, like which notifications we will post and our application name.
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;
326 enumerator = [allNotes objectEnumerator];
327 while ((eventID = [enumerator nextObject])) {
328 [humanReadableNames setObject:[contactAlertsController globalShortDescriptionForEventID:eventID]
331 [descriptions setObject:[contactAlertsController longDescriptionForEventID:eventID
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,
347 * @brief Called when Growl is ready
349 * Currently, this is just used for debugging Growl.
354 AILog(@"Growl is go for launch.");
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.
366 - (void)growlNotificationWasClicked:(NSDictionary *)clickContext
368 NSString *internalObjectID, *uniqueChatID;
369 AIListObject *listObject;
372 if ((internalObjectID = [clickContext objectForKey:KEY_LIST_OBJECT_ID])) {
373 if ((listObject = [[adium contactController] existingListObjectWithUniqueID:internalObjectID]) &&
374 ([listObject isKindOfClass:[AIListContact class]])) {
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];
384 } else if ((uniqueChatID = [clickContext objectForKey:KEY_CHAT_ID])) {
385 chat = [[adium chatController] existingChatWithUniqueChatID:uniqueChatID];
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.
390 (listObject = [[adium contactController] existingListObjectWithUniqueID:uniqueChatID]) &&
391 ([listObject isKindOfClass:[AIListContact class]])) {
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];
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];
406 //Make the chat active
407 [[adium interfaceController] setActiveChat:chat];
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
417 - (NSString *)growlInstallationWindowTitle
419 return GROWL_INSTALLATION_WINDOW_TITLE;
423 * @brief The title of the window shown if Growl needs to be updated
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.
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.
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.
455 - (NSAttributedString *)_growlInformationForUpdate:(BOOL)isUpdate
457 NSMutableAttributedString *growlInfo;
459 //Start with the window title, centered and bold
460 NSMutableParagraphStyle *centeredStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
461 [centeredStyle setAlignment:NSCenterTextAlignment];
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,
469 [[growlInfo mutableString] appendString:@"\n\n"];
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,
478 [growlInfo appendAttributedString:defaultExplanation];