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 "AIPreferenceController.h"
18 #import "AISoundController.h"
19 #import "ESAnnouncerPlugin.h"
20 #import "ESAnnouncerSpeakEventAlertDetailPane.h"
21 #import "ESAnnouncerSpeakTextAlertDetailPane.h"
22 #import "ESContactAlertsController.h"
23 #import <AIUtilities/AIAttributedStringAdditions.h>
24 #import <AIUtilities/AIDictionaryAdditions.h>
25 #import <AIUtilities/ESDateFormatterAdditions.h>
26 #import <AIUtilities/ESImageAdditions.h>
27 #import <Adium/AIContentMessage.h>
28 #import <Adium/AIListObject.h>
30 #define CONTACT_ANNOUNCER_NIB @"ContactAnnouncer" //Filename of the announcer info view
31 #define ANNOUNCER_ALERT_SHORT AILocalizedString(@"Speak Specific Text",nil)
32 #define ANNOUNCER_ALERT_LONG AILocalizedString(@"Speak the text \"%@\"",nil)
34 #define ANNOUNCER_EVENT_ALERT_SHORT AILocalizedString(@"Speak Event","short phrase for the contact alert which speaks the event")
35 #define ANNOUNCER_EVENT_ALERT_LONG AILocalizedString(@"Speak the event aloud","short phrase for the contact alert which speaks the event")
38 * @class ESAnnouncerPlugin
39 * @brief Component which provides Speak Event and Speak Text actions
41 @implementation ESAnnouncerPlugin
48 //Install our contact alerts
49 [[adium contactAlertsController] registerActionID:SPEAK_TEXT_ALERT_IDENTIFIER
51 [[adium contactAlertsController] registerActionID:SPEAK_EVENT_ALERT_IDENTIFIER
54 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:ANNOUNCER_DEFAULT_PREFS
55 forClass:[self class]]
56 forGroup:PREF_GROUP_ANNOUNCER];
58 lastSenderString = nil;
62 * @brief Short description
63 * @result A short localized description of the action
65 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
67 if([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]){
68 return(ANNOUNCER_ALERT_SHORT);
69 }else{ /*Speak Event*/
70 return(ANNOUNCER_EVENT_ALERT_SHORT);
75 * @brief Long description
76 * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
78 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
80 if([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]){
81 NSString *textToSpeak = [details objectForKey:KEY_ANNOUNCER_TEXT_TO_SPEAK];
83 if(textToSpeak && [textToSpeak length]){
84 return([NSString stringWithFormat:ANNOUNCER_ALERT_LONG, textToSpeak]);
86 return(ANNOUNCER_ALERT_SHORT);
88 }else{ /*Speak Event*/
89 return(ANNOUNCER_EVENT_ALERT_LONG);
96 - (NSImage *)imageForActionID:(NSString *)actionID
98 return([NSImage imageNamed:@"AnnouncerAlert" forClass:[self class]]);
102 * @brief Details pane
103 * @result An <tt>AIModularPane</tt> to use for configuring this action, or nil if no configuration is possible.
105 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
107 if([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]){
108 return([ESAnnouncerSpeakTextAlertDetailPane actionDetailsPane]);
109 }else{ /*Speak Event*/
110 return([ESAnnouncerSpeakEventAlertDetailPane actionDetailsPane]);
115 * @brief Perform an action
117 * @param actionID The ID of the action to perform
118 * @param listObject The listObject associated with the event triggering the action. It may be nil
119 * @param details If set by the details pane when the action was created, the details dictionary for this particular action
120 * @param eventID The eventID which triggered this action
121 * @param userInfo Additional information associated with the event; userInfo's type will vary with the actionID.
123 - (void)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
125 NSString *textToSpeak = nil;
126 NSString *timeFormat;
128 BOOL speakTime = [[details objectForKey:KEY_ANNOUNCER_TIME] boolValue];
129 BOOL speakSender = [[details objectForKey:KEY_ANNOUNCER_SENDER] boolValue];
131 timeFormat = (speakTime ?
132 [NSDateFormatter localizedDateFormatStringShowingSeconds:YES showingAMorPM:NO] :
135 if([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]){
136 NSMutableString *userText = [[[details objectForKey:KEY_ANNOUNCER_TEXT_TO_SPEAK] mutableCopy] autorelease];
138 if ([userText rangeOfString:@"%n"].location != NSNotFound) {
139 NSString *replacementText = [listObject formattedUID];
141 [userText replaceOccurrencesOfString:@"%n"
142 withString:(replacementText ? replacementText : @"")
143 options:NSLiteralSearch
144 range:NSMakeRange(0,[userText length])];
147 if ([userText rangeOfString:@"%a"].location != NSNotFound) {
148 NSString *replacementText = [listObject phoneticName];
150 [userText replaceOccurrencesOfString:@"%a"
151 withString:(replacementText ? replacementText : @"")
152 options:NSLiteralSearch
153 range:NSMakeRange(0,[userText length])];
157 if ([userText rangeOfString:@"%t"].location != NSNotFound) {
158 NSString *timeFormat = [NSDateFormatter localizedDateFormatStringShowingSeconds:YES showingAMorPM:NO];
160 [userText replaceOccurrencesOfString:@"%t"
161 withString:[[NSDate date] descriptionWithCalendarFormat:timeFormat
164 options:NSLiteralSearch
165 range:NSMakeRange(0,[userText length])];
170 if ([userText rangeOfString:@"%m"].location != NSNotFound) {
173 if ([[adium contactAlertsController] isMessageEvent:eventID]) {
174 AIContentMessage *content = [userInfo objectForKey:@"AIContentObject"];
175 message = [[[content message] attributedStringByConvertingAttachmentsToStrings] string];
178 message = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
179 listObject:listObject
184 [userText replaceOccurrencesOfString:@"%m"
185 withString:(message ? message : @"")
186 options:NSLiteralSearch
187 range:NSMakeRange(0,[userText length])];
190 textToSpeak = userText;
192 //Clear out the lastSenderString so the next Speak Event action will get tagged with the sender's name
193 [lastSenderString release]; lastSenderString = nil;
195 }else{ /*Speak Event*/
197 //Handle messages in a custom manner
198 if([[adium contactAlertsController] isMessageEvent:eventID]){
199 AIContentMessage *content = [userInfo objectForKey:@"AIContentObject"];
200 NSString *message = [[[content message] attributedStringByConvertingAttachmentsToStrings] string];
201 AIListObject *source = [content source];
202 BOOL isOutgoing = [content isOutgoing];
203 BOOL newParagraph = NO;
204 NSMutableString *theMessage = [NSMutableString string];
206 if(speakSender && !isOutgoing) {
207 NSString *senderString;
209 //Get the sender string
210 senderString = [source phoneticName];
212 //Don't repeat the same sender string for messages twice in a row
213 if(!lastSenderString || ![senderString isEqualToString:lastSenderString]){
214 NSMutableString *senderStringToSpeak;
216 //Track the sender string before modifications
217 [lastSenderString release]; lastSenderString = [senderString retain];
219 senderStringToSpeak = [senderString mutableCopy];
221 //deemphasize all words after first in sender's name, approximating human name pronunciation better
222 [senderStringToSpeak replaceOccurrencesOfString:@" "
223 withString:@" [[emph -]] "
224 options:NSCaseInsensitiveSearch
225 range:NSMakeRange(0, [senderStringToSpeak length])];
226 //emphasize first word in sender's name
227 [theMessage appendFormat:@"[[emph +]] %@...",senderStringToSpeak];
230 [senderStringToSpeak release];
234 //Append the date if desired, after the sender name if that was added
236 [theMessage appendFormat:@" %@...",[[content date] descriptionWithCalendarFormat:timeFormat
241 if(newParagraph) [theMessage appendFormat:@" [[pmod +1; pbas +1]]"];
243 //Finally, append the actual message
244 [theMessage appendFormat:@" %@",message];
246 //theMessage is now the final string which will be passed to the speech engine
247 textToSpeak = theMessage;
250 //All non-message events use the normal naturalLanguageDescription methods, optionally prepending
252 NSString *eventDescription;
254 eventDescription = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
255 listObject:listObject
260 NSString *timeString;
262 timeString = [NSString stringWithFormat:@"%@... ",[[NSDate date] descriptionWithCalendarFormat:timeFormat
265 textToSpeak = [timeString stringByAppendingString:eventDescription];
267 textToSpeak = eventDescription;
270 //Clear out the lastSenderString so the next speech event will get tagged with the sender's name
271 [lastSenderString release]; lastSenderString = nil;
275 //Do the speech, with custom voice/pitch/rate as desired
277 NSNumber *pitchNumber = nil, *rateNumber = nil;
278 NSNumber *customPitch, *customRate;
280 if ((customPitch = [details objectForKey:KEY_PITCH_CUSTOM]) &&
281 ([customPitch boolValue])) {
282 pitchNumber = [details objectForKey:KEY_PITCH];
285 if ((customRate = [details objectForKey:KEY_RATE_CUSTOM]) &&
286 ([customRate boolValue])) {
287 rateNumber = [details objectForKey:KEY_RATE];
290 [[adium soundController] speakText:textToSpeak
291 withVoice:[details objectForKey:KEY_VOICE_STRING]
292 pitch:(pitchNumber ? [pitchNumber floatValue] : 0.0)
293 rate:(rateNumber ? [rateNumber floatValue] : 0.0)];
298 * @brief Allow multiple actions?
300 * If this method returns YES, every one of this action associated with the triggering event will be executed.
301 * If this method returns NO, only the first will be.
303 * These are sound-based actions, so only allow one.
305 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID