Merged [13645] and [13646]: Fixed one of the longest-standing Adium bugs: The Color...
[adiumx.git] / Source / ESAnnouncerPlugin.m
blob0b96693d1cbed228cf107412043bed70bcf70882
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 "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")
37 /*!
38  * @class ESAnnouncerPlugin
39  * @brief Component which provides Speak Event and Speak Text actions
40  */
41 @implementation ESAnnouncerPlugin
43 /*!
44  * @brief Install
45  */
46 - (void)installPlugin
48     //Install our contact alerts
49         [[adium contactAlertsController] registerActionID:SPEAK_TEXT_ALERT_IDENTIFIER
50                                                                                   withHandler:self];
51         [[adium contactAlertsController] registerActionID:SPEAK_EVENT_ALERT_IDENTIFIER
52                                                                                   withHandler:self];
53     
54         [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:ANNOUNCER_DEFAULT_PREFS 
55                                                                                                                                                 forClass:[self class]] 
56                                                                                   forGroup:PREF_GROUP_ANNOUNCER];
57         
58     lastSenderString = nil;
61 /*!
62  * @brief Short description
63  * @result A short localized description of the action
64  */
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);
71         }
74 /*!
75  * @brief Long description
76  * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
77  */
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];
82                 
83                 if(textToSpeak && [textToSpeak length]){
84                         return([NSString stringWithFormat:ANNOUNCER_ALERT_LONG, textToSpeak]);
85                 }else{
86                         return(ANNOUNCER_ALERT_SHORT);
87                 }
88         }else{ /*Speak Event*/
89                 return(ANNOUNCER_EVENT_ALERT_LONG);
90         }
93 /*!
94  * @brief Image
95  */
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.
104  */
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]);
111         }
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.
122  */
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];
130         
131         timeFormat = (speakTime ?
132                                   [NSDateFormatter localizedDateFormatStringShowingSeconds:YES showingAMorPM:NO] :
133                                   nil);
134         
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];
140                         
141                         [userText replaceOccurrencesOfString:@"%n"
142                                                                           withString:(replacementText ? replacementText : @"")
143                                                                                  options:NSLiteralSearch 
144                                                                                    range:NSMakeRange(0,[userText length])];
145                 }
147                 if ([userText rangeOfString:@"%a"].location != NSNotFound) {
148                         NSString        *replacementText = [listObject phoneticName];
149                         
150                         [userText replaceOccurrencesOfString:@"%a"
151                                                                           withString:(replacementText ? replacementText : @"")
152                                                                                  options:NSLiteralSearch 
153                                                                                    range:NSMakeRange(0,[userText length])];
154                         
155                 }
156                 
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
162                                                                                                                                                                          timeZone:nil
163                                                                                                                                                                            locale:nil]
164                                                                                  options:NSLiteralSearch 
165                                                                                    range:NSMakeRange(0,[userText length])];
166                         
167                 }
168                 
169                 
170                 if ([userText rangeOfString:@"%m"].location != NSNotFound) {
171                         NSString                        *message;
172                         
173                         if ([[adium contactAlertsController] isMessageEvent:eventID]) {
174                                 AIContentMessage        *content = [userInfo objectForKey:@"AIContentObject"];
175                                 message = [[[content message] attributedStringByConvertingAttachmentsToStrings] string];
176                                 
177                         } else {
178                                 message = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
179                                                                                                                                                                          listObject:listObject
180                                                                                                                                                                            userInfo:userInfo
181                                                                                                                                                                  includeSubject:NO];
182                         }
183                         
184                         [userText replaceOccurrencesOfString:@"%m"
185                                                                           withString:(message ? message : @"")
186                                                                                  options:NSLiteralSearch 
187                                                                                    range:NSMakeRange(0,[userText length])];                             
188                 }
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;
194                 
195         }else{ /*Speak Event*/  
196                 
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];
205                         
206                         if(speakSender && !isOutgoing) {
207                                 NSString        *senderString;
208                                 
209                                 //Get the sender string
210                                 senderString = [source phoneticName];
211                         
212                                 //Don't repeat the same sender string for messages twice in a row
213                                 if(!lastSenderString || ![senderString isEqualToString:lastSenderString]){
214                                         NSMutableString         *senderStringToSpeak;
215                                         
216                                         //Track the sender string before modifications
217                                         [lastSenderString release]; lastSenderString = [senderString retain];
218                                         
219                                         senderStringToSpeak = [senderString mutableCopy];
220                                         
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];
228                                         newParagraph = YES;
229                                         
230                                         [senderStringToSpeak release];
231                                 }
232                         }
233                         
234                         //Append the date if desired, after the sender name if that was added
235                         if(timeFormat){
236                                 [theMessage appendFormat:@" %@...",[[content date] descriptionWithCalendarFormat:timeFormat
237                                                                                                                                                                                 timeZone:nil
238                                                                                                                                                                                   locale:nil]];
239                         }
240                         
241                         if(newParagraph) [theMessage appendFormat:@" [[pmod +1; pbas +1]]"];
243                         //Finally, append the actual message
244                         [theMessage appendFormat:@" %@",message];
245                         
246                         //theMessage is now the final string which will be passed to the speech engine
247                         textToSpeak = theMessage;
249                 }else{
250                         //All non-message events use the normal naturalLanguageDescription methods, optionally prepending
251                         //the time
252                         NSString        *eventDescription;
253                         
254                         eventDescription = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
255                                                                                                                                                                                   listObject:listObject
256                                                                                                                                                                                         userInfo:userInfo
257                                                                                                                                                                           includeSubject:YES];
258                         
259                         if(timeFormat){
260                                 NSString        *timeString;
261                                 
262                                 timeString = [NSString stringWithFormat:@"%@... ",[[NSDate date] descriptionWithCalendarFormat:timeFormat
263                                                                                                                                                                                                           timeZone:nil
264                                                                                                                                                                                                                 locale:nil]];
265                                 textToSpeak = [timeString stringByAppendingString:eventDescription];
266                         }else{
267                                 textToSpeak = eventDescription;
268                         }
269                         
270                         //Clear out the lastSenderString so the next speech event will get tagged with the sender's name
271                         [lastSenderString release]; lastSenderString = nil;
272                 }
273         }
274         
275         //Do the speech, with custom voice/pitch/rate as desired
276         if(textToSpeak){
277                 NSNumber        *pitchNumber = nil, *rateNumber = nil;
278                 NSNumber        *customPitch, *customRate;
279                 
280                 if ((customPitch = [details objectForKey:KEY_PITCH_CUSTOM]) &&
281                         ([customPitch boolValue])) {
282                         pitchNumber = [details objectForKey:KEY_PITCH];
283                 }
285                 if ((customRate = [details objectForKey:KEY_RATE_CUSTOM]) &&
286                         ([customRate boolValue])) {
287                         rateNumber = [details objectForKey:KEY_RATE];
288                 }
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)];
294         }
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.
304  */
305 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
307         return(NO);
310 @end