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 <Adium/AIPreferenceControllerProtocol.h>
18 #import "AISoundController.h"
19 #import "ESAnnouncerPlugin.h"
20 #import "ESAnnouncerSpeakEventAlertDetailPane.h"
21 #import "ESAnnouncerSpeakTextAlertDetailPane.h"
22 #import <Adium/AIContactAlertsControllerProtocol.h>
23 #import <AIUtilities/AIAttributedStringAdditions.h>
24 #import <AIUtilities/AIDictionaryAdditions.h>
25 #import <AIUtilities/AIDateFormatterAdditions.h>
26 #import <AIUtilities/AIImageAdditions.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 - (BOOL)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
125 NSString *textToSpeak = nil;
127 //Do nothing if sounds are muted for this object
128 if ([listObject soundsAreMuted]) return NO;
130 if ([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]) {
131 NSMutableString *userText = [[[details objectForKey:KEY_ANNOUNCER_TEXT_TO_SPEAK] mutableCopy] autorelease];
133 if ([userText rangeOfString:@"%n"].location != NSNotFound) {
134 NSString *replacementText = [listObject formattedUID];
136 [userText replaceOccurrencesOfString:@"%n"
137 withString:(replacementText ? replacementText : @"")
138 options:NSLiteralSearch
139 range:NSMakeRange(0,[userText length])];
142 if ([userText rangeOfString:@"%a"].location != NSNotFound) {
143 NSString *replacementText = [listObject phoneticName];
145 [userText replaceOccurrencesOfString:@"%a"
146 withString:(replacementText ? replacementText : @"")
147 options:NSLiteralSearch
148 range:NSMakeRange(0,[userText length])];
152 if ([userText rangeOfString:@"%t"].location != NSNotFound) {
153 NSString *timeFormat = [NSDateFormatter localizedDateFormatStringShowingSeconds:YES showingAMorPM:NO];
155 [userText replaceOccurrencesOfString:@"%t"
156 withString:[[NSDate date] descriptionWithCalendarFormat:timeFormat
159 options:NSLiteralSearch
160 range:NSMakeRange(0,[userText length])];
165 if ([userText rangeOfString:@"%m"].location != NSNotFound) {
168 if ([[adium contactAlertsController] isMessageEvent:eventID]) {
169 AIContentMessage *content = [userInfo objectForKey:@"AIContentObject"];
170 message = [[[content message] attributedStringByConvertingAttachmentsToStrings] string];
173 message = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
174 listObject:listObject
179 [userText replaceOccurrencesOfString:@"%m"
180 withString:(message ? message : @"")
181 options:NSLiteralSearch
182 range:NSMakeRange(0,[userText length])];
185 textToSpeak = userText;
187 //Clear out the lastSenderString so the next Speak Event action will get tagged with the sender's name
188 [lastSenderString release]; lastSenderString = nil;
190 } else { /*Speak Event*/
191 BOOL speakSender = [[details objectForKey:KEY_ANNOUNCER_SENDER] boolValue];
192 BOOL speakTime = [[details objectForKey:KEY_ANNOUNCER_TIME] boolValue];
193 NSString *timeFormat;
195 timeFormat = (speakTime ?
196 [NSDateFormatter localizedDateFormatStringShowingSeconds:YES showingAMorPM:NO] :
199 //Handle messages in a custom manner
200 if ([[adium contactAlertsController] isMessageEvent:eventID]) {
201 AIContentMessage *content = [userInfo objectForKey:@"AIContentObject"];
202 NSString *message = [[[content message] attributedStringByConvertingAttachmentsToStrings] string];
203 AIListObject *source = [content source];
204 BOOL isOutgoing = [content isOutgoing];
205 BOOL newParagraph = NO;
206 NSMutableString *theMessage = [NSMutableString string];
208 if (speakSender && !isOutgoing) {
209 NSString *senderString;
211 //Get the sender string
212 senderString = [source phoneticName];
214 //Don't repeat the same sender string for messages twice in a row
215 if (!lastSenderString || ![senderString isEqualToString:lastSenderString]) {
216 NSMutableString *senderStringToSpeak;
218 //Track the sender string before modifications
219 [lastSenderString release]; lastSenderString = [senderString retain];
221 senderStringToSpeak = [senderString mutableCopy];
223 //deemphasize all words after first in sender's name, approximating human name pronunciation better
224 [senderStringToSpeak replaceOccurrencesOfString:@" "
225 withString:@" [[emph -]] "
226 options:NSCaseInsensitiveSearch
227 range:NSMakeRange(0, [senderStringToSpeak length])];
228 //emphasize first word in sender's name
229 [theMessage appendFormat:@"[[emph +]] %@...",senderStringToSpeak];
232 [senderStringToSpeak release];
236 //Append the date if desired, after the sender name if that was added
238 [theMessage appendFormat:@" %@...",[[content date] descriptionWithCalendarFormat:timeFormat
243 if (newParagraph) [theMessage appendFormat:@" [[pmod +1; pbas +1]]"];
245 //Finally, append the actual message
246 [theMessage appendFormat:@" %@",message];
248 //theMessage is now the final string which will be passed to the speech engine
249 textToSpeak = theMessage;
252 //All non-message events use the normal naturalLanguageDescription methods, optionally prepending
254 NSString *eventDescription;
256 eventDescription = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
257 listObject:listObject
262 NSString *timeString;
264 timeString = [NSString stringWithFormat:@"%@... ",[[NSDate date] descriptionWithCalendarFormat:timeFormat
267 textToSpeak = [timeString stringByAppendingString:eventDescription];
269 textToSpeak = eventDescription;
272 //Clear out the lastSenderString so the next speech event will get tagged with the sender's name
273 [lastSenderString release]; lastSenderString = nil;
277 //Do the speech, with custom voice/pitch/rate as desired
279 NSNumber *pitchNumber = nil, *rateNumber = nil;
280 NSNumber *customPitch, *customRate;
282 if ((customPitch = [details objectForKey:KEY_PITCH_CUSTOM]) &&
283 ([customPitch boolValue])) {
284 pitchNumber = [details objectForKey:KEY_PITCH];
287 if ((customRate = [details objectForKey:KEY_RATE_CUSTOM]) &&
288 ([customRate boolValue])) {
289 rateNumber = [details objectForKey:KEY_RATE];
292 [[adium soundController] speakText:textToSpeak
293 withVoice:[details objectForKey:KEY_VOICE_STRING]
294 pitch:(pitchNumber ? [pitchNumber floatValue] : 0.0)
295 rate:(rateNumber ? [rateNumber floatValue] : 0.0)];
298 return (textToSpeak != nil);
302 * @brief Allow multiple actions?
304 * If this method returns YES, every one of this action associated with the triggering event will be executed.
305 * If this method returns NO, only the first will be.
307 * These are sound-based actions, so only allow one.
309 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID